Comments
-- This is a comment. The computer ignores it.
-- Use comments to explain your code to other humans!
Values and Variables
let name: String := "Alice"      -- a piece of text (String)
let age: Integer := 10           -- a whole number (Integer)
let height: Real := 4.5          -- a decimal number (Real)
let likes_cats: Boolean := true  -- true or false (Boolean)

Real literals must include at least one digit after the decimal point. Valid examples: 4.5, 10.0, .5, 12.0e-3. Invalid examples: 10., 12.e-3.

Printing
print("Hello, world!")
print(name)
print("I am " + age + " years old")
Math
let sum: Integer := 3 + 4       -- 7
let diff: Integer := 10 - 3     -- 7
let product: Integer := 5 * 6   -- 30
let quotient: Integer := 20 / 4 -- 5
let remainder: Integer := 10 % 3 -- 1
let power: Integer := 2 ^ 8     -- 256

Integer literals can also be written with explicit bases:

let flags: Integer := 0b1111_0000
let perms: Integer := 0o755
let color: Integer := 0xFF_AA_33

Use lowercase prefixes 0b, 0o, and 0x. _ may be used as a digit separator.

Integer is a 64-bit integer; Integer64 is another spelling of the same type.

Bytes and fixed-width integers. Byte, Integer16 and Integer32 are integers of an exact width, for binary data and protocols. Each is its own type, written with a suffix on an integer literal:

let b: Byte := 200u8            -- unsigned, 0..255
let port: Integer16 := 8080i16  -- signed, -32768..32767
let id: Integer32 := -70000i32  -- signed, -2147483648..2147483647
let mask: Byte := 0xFFu8        -- bases and `_` work as usual

An unsuffixed literal is always an Integer, so let b: Byte := 200 is a type error. A leading - is part of a signed literal: -5i16 is an Integer16.

None of Integer, Integer16, Integer32 and Byte converts to another on its own. Convert with a method; narrowing raises if the value does not fit:

let n: Integer := 300
let s: Integer16 := n.to_integer16()   -- ok
let bb: Byte := n.to_byte()            -- raises: 300 is not in 0..255
let back: Integer := s.to_integer()

Arithmetic on any of them gives an Integer, so it cannot overflow the narrow type; narrow the result again if you need to: (s + s).to_integer16(). Comparison and = are between two values of the same type.

Byte is also the element type of "text".to_bytes(), and create String.from_bytes(bytes) turns an Array[Byte] back into a String. Their methods are in the scalar types reference.

Comparing Things
x = y                            -- equal?
x /= y                           -- not equal?
x == y                           -- same object?
x != y                           -- different object?
x < y    x <= y                  -- less than? less or equal?
x > y    x >= y                  -- greater than? greater or equal?
x and y                          -- both true?
x or y                           -- at least one true?
not x                            -- flip true/false
Comparison

Nex has two kinds of equality:

  • = and /= compare by value
  • == and != compare by identity

Value equality checks whether two values have the same contents:

print([1, 2] = [1, 2])           -- true
print([1, 2] /= [1, 2])          -- false
print("abc" = "abc")             -- true

For objects, =//= use the class's equals method. By default that is a structural, field-by-field comparison, but a class may override equals (and the matching hash) to define its own value equality. ==/!= ignore any override and always compare object identity.

Identity equality checks whether two variables refer to the same runtime object:

let a := [1, 2]
let b := a
let c := [1, 2]

print(a == b)                    -- true
print(a == c)                    -- false
print(a != c)                    -- true

For scalar values like numbers, booleans, characters, strings, and nil, == and != compare the values directly:

print(1 == 1)                    -- true
print("x" != "y")                -- true
Choosing: if / then / else
if age >= 18 then
  print("You can vote!")
elseif age >= 13 then
  print("You are a teenager")
else
  print("You are a kid")
end
Inline Choice: when
let label: String := when age >= 18 then "adult" else "minor" end
Loops: from / until
from let i: Integer := 1 until i > 5 do
  print(i)
  i := i + 1
end
-- prints 1 2 3 4 5
Repeat
repeat 3 do
  print("hello!")
end
-- prints hello! three times
Across

Iterate over any collection (Array, String, Map) using its cursor:

across [10, 20, 30] as x do
  print(x)
end
-- prints 10 20 30

Strings iterate by character:

across "abc" as ch do
  print(ch)
end
-- prints #a #b #c

Maps iterate as Map_Entry[K, V] values, read-only key/value pairs with typed key and value members:

across {"name": "Alice", "age": "10"} as entry do
  print(entry.key + ": " + entry.value)
end
-- prints "name: Alice"  "age: 10"

An entry itself prints as "name": "Alice" (that is, print(entry)). For compatibility with earlier code, entry.get(0) and entry.get(1) still return the key and value, typed Any; prefer key and value.

Functions
function greet(name: String)
do
  print("Hello, " + name + "!")
end

greet("Bob")

A function that gives back a value:

function double(n: Integer): Integer
do
  result := n * 2
end

print(double(5))                 -- 10

result is reserved for the value a routine returns. It cannot be declared as anything else — a let, a parameter, a field, an across or match variable — because the declaration would hide the return value and the routine would silently return the default. Assign it with result := ...; choose another name for everything else.

Every function in a program is checked against every other function's signature regardless of which is written first, so mutually recursive functions need no forward declaration at all:

function is_even(n: Integer): Boolean do
  if n = 0 then
    result := true
  else
    result := is_odd(n - 1)
  end
end

function is_odd(n: Integer): Boolean do
  if n = 0 then
    result := false
  else
    result := is_even(n - 1)
  end
end

declare function is available separately, to pin a function's signature explicitly at a point before its real definition — useful for documentation, or when the definition is far away — and the later definition is checked against it exactly:

declare function is_even(n: Integer): Boolean
declare function is_odd(n: Integer): Boolean

function is_even(n: Integer): Boolean do
  if n = 0 then result := true else result := is_odd(n - 1) end
end

function is_odd(n: Integer): Boolean do
  if n = 0 then result := false else result := is_even(n - 1) end
end

Mutual recursion between let-bound closures works the same way: consecutive closure-literal lets in one block are elaborated together, so any of them may call any other, and one may call itself:

let is_even := fn(n: Integer): Boolean do
  if n = 0 then result := true else result := is_odd(n - 1) end
end
let is_odd := fn(n: Integer): Boolean do
  if n = 0 then result := false else result := is_even(n - 1) end
end

This works for a whole file (as bare top-level statements) and for a single, multi-statement REPL input (wrap the statements in do ... end to submit them to the REPL as one input, since the REPL otherwise treats each complete statement as its own separate input) — including two or more closures that merely share a captured, mutated variable without calling each other at all (let total := 0 / let add := fn(x) do total := total + x end / let peek := fn(): Integer do result := total end). It does not currently work across separate REPL inputs — defining both closures in one input and then calling one from a later, separate input fails (a limitation of the interactive session bridging interpreted and compiled state, not of the language). A single self-recursive or self-mutating closure (e.g. a counter) is unaffected either way; the gap is specific to two or more closures that reference each other or share mutated state, split across separate inputs. If you need that, use function (with declare function if needed) instead of let — named functions are not affected.

No default arguments. Nex has no default parameter values. A call must pass exactly as many arguments as the function (or method) declares.

Free functions cannot be overloaded either: each function name must be unique, so you cannot define two greet functions that differ only in the number of parameters. If you need variants, give them distinct names.

Class methods, however, can be overloaded by arity (see Optional arguments via method overloading under Classes), which is the idiomatic way to get optional-argument ergonomics in Nex.

Arrays
let colors: Array [String] := ["red", "green", "blue"]
print(colors.get(0))            -- "red"
colors.add("yellow")            -- add to the end
print(colors.length)            -- 4

Bytes: Byte_Array. An Array[Byte] is a list of boxed values. For binary data, intern data/Byte_Array gives a fixed-size buffer stored as a real Java byte[]:

intern data/Byte_Array

let buf: Byte_Array := create Byte_Array.make(4)
buf.set(0, 200u8)
print(buf.get(0))                 -- 200
print(buf.slice(0, 2))            -- Byte_Array([200, 0])
across buf as b do print(b) end   -- b is a Byte
Maps
let pet: Map [String, String] := {"name": "Max", "kind": "dog"}
print(pet.get("name"))            -- "Max"
pet.put("kind", "cat")            -- update a value
Sets

Use a set when you want an unordered collection of distinct values.

let evens: Set[Integer] := #{0, 2, 4}
print(evens.contains(2))          -- true

An empty set uses the explicit set literal syntax:

let empty: Set[Integer] := #{}

You can also build a set from an array. Duplicate elements are removed:

let numbers: Set[Integer] := create Set[Integer].from_array([1, 2, 2, 3])
print(numbers.size)               -- 3

Sets support the usual set operations:

let a: Set[Integer] := #{1, 2, 3}
let b: Set[Integer] := #{3, 4}

print(a.union(b))                 -- #{1, 2, 3, 4}
print(a.intersection(b))          -- #{3}
print(a.difference(b))            -- #{1, 2}
Concurrency: spawn, Task, and Channel

Use spawn to start a lightweight task:

let t: Task[Integer] := spawn do
  result := 1 + 2
end

print(t.await)                    -- 3
print(t.is_done)                  -- true (after completion)

If the spawn body does not assign result, the type is plain Task:

let t: Task := spawn do
  print("background work")
end

Task operations:

  • await waits until the task finishes
  • await(ms) waits up to ms milliseconds and returns nil on timeout
  • cancel requests task cancellation and returns true if the task was cancelled before finishing
  • is_done reports whether the task has finished
  • is_cancelled reports whether the task was cancelled
  • await_any([t1, t2, ...]) waits for the first task to finish and returns its result
  • await_all([t1, t2, ...]) waits for all tasks and returns an array of results

Use Channel[T] to communicate between tasks:

let ch: Channel[Integer] := create Channel[Integer]

spawn do
  ch.send(42)
end

print(ch.receive)                 -- 42
ch.close
print(ch.is_closed)               -- true

Use .with_capacity(n) for buffered channels:

let ch: Channel[Integer] := create Channel[Integer].with_capacity(2)
ch.send(10)
ch.send(20)
print(ch.size)                    -- 2
print(ch.capacity)                -- 2

Channel operations:

  • send(value) blocks until accepted
  • send(value, ms) waits up to ms milliseconds and returns true on success, false on timeout
  • receive blocks until a value is available
  • receive(ms) waits up to ms milliseconds and returns nil on timeout
  • try_send(value) returns true if the send succeeds immediately, otherwise false
  • try_receive returns a value if one is immediately available, otherwise nil
  • close prevents future sends; buffered values may still be received
  • is_closed, size, and capacity report channel state

Use select to wait on multiple channel operations or completed tasks:

select
  when jobs.receive as job then
    print(job)
  when worker.await as value then
    print(value)
  when control.receive as signal then
    print(signal)
  timeout 1000 then
    print("timed out")
  else
    print("idle")
end

select probes its clauses using try_send / try_receive for channels and is_done for tasks. Task clauses must use Task.await; they fire only when the task has already completed. If no clause is ready and there is no else, select waits until one becomes ready.

For full concurrency semantics and runtime details, see the Concurrency Guide.

Classes

A class bundles data and actions together:

class Pet
  feature
    name: String
    sound: String

  create
    make(name: String, sound: String) do
      this.name := name
      this.sound := sound
    end

  feature
    speak do
      print(name + " says " + sound)
    end
end
Creating Objects
let cat: Pet := create Pet.make("Mimi", "meow")
cat.speak                        -- "Mimi says meow"
Constructors

A constructor sets up an object when it is created:

class Circle
  create
    make(r: Real) do
      radius := r
    end

  feature
    radius: Real

    area(): Real do
      result := 3.14159 * (radius ^ 2)
    end
end

let c: Circle := create Circle.make(5.0)
print(c.area)                    -- 78.53975

A field is set by assigning it (radius := r). Declaring a local with the field's name (let radius := r) would only hide the field, so a let in a class's routines may not reuse the name of a field or constant visible there, including an inherited one. A parameter may, as in make(name: String) do this.name := name end.

A constructor can delegate to another constructor of the same class with this.<ctor-name>(...), so shared setup lives in one place:

class Box
  feature
    value: Integer
  create
    make(v: Integer) do
      value := v
    end
    default do
      this.make(0)              -- delegates to make
    end
end

let b: Box := create Box.default
print(b.value)                   -- 0

(To call a parent class's constructor instead, see Inheritance.)

Optional arguments via method overloading

Nex has no default parameter values, but a class may define several methods with the same name and different numbers of parameters. The right one is chosen by the number of arguments at the call site. Have the shorter version forward to the longer one to supply a default:

class Greeter
  feature
    greet(name: String): String do
      result := greet(name, "!")        -- forward with a chosen default
    end

    greet(name: String, punct: String): String do
      result := "Hello, " + name + punct
    end
end

let g: Greeter := create Greeter
print(g.greet("Ann"))              -- "Hello, Ann!"
print(g.greet("Bob", "."))         -- "Hello, Bob."

Overloads are distinguished only by the number of arguments, not their types, so you cannot have two same-name methods with the same arity that differ only in parameter type. (Free functions cannot be overloaded at all — see Functions.)

Inheritance

A class can build on another class:

class Animal
  feature
    name: String
    speak do print(name) end
  create
    with_name(n: String) do name := n end
end

class Dog
  inherit Animal
  feature
    speak do
      print(name + " says Woof!")
    end
  create
    with_name(n: String) do Animal.with_name(n) end
end

Redeclaring queries as attributes. A heir may answer an inherited query (a routine with no arguments) with a stored attribute of the same name. Its type must conform to the query's return type. This is also how a deferred query is most often implemented:

deferred class Shape
  feature
    area(): Real deferred
    describe: String do result := "area=" + area.to_string end
end

class Square
  inherit Shape
  feature
    area: Real                     -- answers Shape's area
  create
    make(a: Real) do area := a end
end

let sh: Shape := create Square.make(4.0)
print(sh.describe)                 -- "area=4.0"

Clients and the parent's own code (describe above) read the attribute through the query, and the query's inherited postconditions still apply to it. Inside Square, area is the attribute, which only Square can assign. super.area still reaches the parent's query.

The reverse is not allowed. An attribute cannot be redeclared as a routine: the class that declares the attribute, and its contracts, would keep using the stored value while every client saw the routine. So the parent's promises would describe something clients can no longer observe. To keep a value open to redefinition, store it under another name and publish it as a query:

class Account
  feature
    stored_balance: Integer
    balance: Integer do result := stored_balance end
    deposit(amount: Integer) do stored_balance := stored_balance + amount end
  create
    make(b: Integer) do stored_balance := b end
end

class Checking_Account
  inherit Account
  feature
    required: Integer
    balance: Integer do                -- overrides the query
      if super.balance > required then result := super.balance end
    end
  create
    make(b, r: Integer) do super.make(b) required := r end
end

Clients write a.balance either way, whether the value is stored or computed.

Operator Aliases

A feature can bind itself to an arithmetic operator with an alias clause. The operator is then exactly sugar for the call: a - b is a.minus(b), so the feature's preconditions and postconditions apply at the operator too.

class Money
  feature
    once amount: Integer
    once currency: String

    minus(other: Money): Money
      alias "-"
      require
        same_currency: currency = other.currency
      do
        result := create Money.make(amount - other.amount, currency)
      end
  create
    make(a: Integer, c: String) do amount := a  currency := c end
end

let owed: Money := create Money.make(100, "USD")
let paid: Money := create Money.make(30, "USD")
let rest: Money := owed - paid          -- calls minus, checks same_currency

Three rules keep this from becoming the licence to invent notation that operator overloading usually is:

  • The operator set is closed. Only +, -, *, /, %, and ^ can be aliased. No program can introduce a symbol a reader has never seen.
  • Only arithmetic. Ordering already dispatches through Comparable's compare, and = through equals; a class gets <, <=, >, >=, and = by inheriting Comparable and overriding those, not by aliasing.
  • Built-in arithmetic wins, always. An alias is consulted only where the operands are not numeric (or String, for +), so no class can change what + means on Integer or Real.

An alias is inherited: if a deferred parent declares plus … alias "+", then + works on any descendant, dispatching to its override.

alias is a soft keyword: it carries this meaning only in the position above, and stays available as an ordinary name anywhere else.

let alias := "vj"          -- still a perfectly good variable
print(u.alias)              -- and a perfectly good field

union is a keyword too, but narrower than alias: it still works as a member name (Set.union), but not as a general identifier — let union := 5 does not parse. where is not restricted at all; it is recognized only inside a refinement type's where n: <expr> clause (see Refinement Types) and is an ordinary identifier everywhere else, including let where := 5.

A class whose values are compared with = — including in a postcondition like ensure reduced: balance = old balance - amount — should also override equals (and hash), or = compares object identity rather than value.

Once Fields

A field declared with once can be set in a constructor but never reassigned afterward. The typechecker enforces this at compile time; the interpreter enforces it at runtime.

class Point
  feature
    once x: Integer
    once y: Integer
  create
    make(px: Integer, py: Integer) do
      x := px
      y := py
    end
end

let p: Point := create Point.make(3, 7)
-- p.x and p.y are now permanently 3 and 7

Attempting to assign a once field outside a constructor is a compile-time error:

class Box
  feature
    once value: Integer
  create
    make(v: Integer) do value := v end
  feature
    overwrite(v: Integer) do
      value := v     -- error: 'value' is a once field
    end
end
Class Constants

A feature written NAME = expression (an initializer, no constructor) is a class-level constant, shared by every instance and read through the class name. The initializer is any expression — scalar, object, or collection — and is interned: evaluated once for the whole run, so every read returns the same value.

class Screen
  feature
    WIDTH  = 1920
    HEIGHT = 1080
    AREA   = WIDTH * HEIGHT      -- may reference an earlier constant
end

print(Screen.WIDTH)              -- 1920
print(Screen.AREA)               -- 2073600

Because an object- or collection-valued constant is one shared value, C.x == C.x holds for it:

class Origin
  feature
    POINT = create Point.make(0, 0)
end

print(Origin.POINT.x)                 -- 0
print(Origin.POINT == Origin.POINT)   -- true: the one canonical value

The initializer may reference an earlier constant of the same class or an inherited one; a forward or cyclic reference is a compile-time error.

Design by Contract

Tell Nex what must be true before, after, and always:

class Wallet
  feature
    money: Real

    spend(amount: Real)
      require                    -- must be true BEFORE
        enough: amount <= money
      do
        money := money - amount
      ensure                     -- must be true AFTER
        less: money = old money - amount
      end

  invariant                      -- must ALWAYS be true
    not_negative: money >= 0.0
end

require, ensure, and invariant speak about a routine's boundaries or a class. assert states what must be true at one point inside a body:

assert enough_left: money >= 0.0   -- named: the failure reports the name
assert money >= 0.0                -- bare: the failure reports the line

assert                             -- several at once, like require
  not_negative: money >= 0.0
  under_limit: money <= 10000.0

A failed assert raises the same contract violation as a failed require or ensure. Assertions always run; there is no mode that strips them.

Error Handling
let attempts: Integer := 0
do
  attempts := attempts + 1
  if attempts < 3 then
    raise "not ready yet"
  end
  print("done on attempt " + attempts)
rescue
  print("failed, trying again...")
  retry                        -- jump back to do and try again
end

raise throws an error. rescue catches it (the value is in exception). retry jumps back to the do block and runs it again from the top.

Case / Of
case direction of
  "up"    then print("going up")
  "down"  then print("going down")
  else         print("standing still")
end
Sealed Classes

A sealed modifier closes a class hierarchy. Only classes defined together with the sealed class can extend it. The typechecker knows the complete set of subclasses and can verify that every variant is handled.

A sealed class must be declared deferred — it cannot be instantiated directly, and the typechecker rejects a sealed class that is not also deferred. (Otherwise a bare instance of the parent would be a runtime value that an exhaustive match over its subclasses does not cover.)

sealed deferred class Result
end

class Ok
  inherit Result
  feature value: Integer
  create make(v: Integer) do value := v end
end

class Err
  inherit Result
  feature msg: String
  create make(m: String) do msg := m end
end
Sum Types (union)

Writing a sealed hierarchy by hand is verbose: a sealed deferred class parent plus a full class … inherit … feature … create make for every variant. The union form is concise sugar for exactly that shape.

union Order
  Draft
  Placed(id: String, total: Real)
  Shipped(tracking: String, at: Date)
end

This desugars to a sealed deferred class Order parent and one ordinary class per variant. Each variant's payload becomes feature fields plus an auto-generated make constructor, so construction and matching are the same as for hand-written sealed classes:

let o: Order := create Placed.make("A-100", 42.0)

match o of
  Draft   as d then print("draft")
  Placed  as p then print(p.id)       -- payloads are ordinary fields
  Shipped as s then print(s.tracking)
end
  • A variant with no payload (Draft) still gets a nullary make, so create Draft.make() works.
  • Generic parameters carry through to every variant: union Result[T] gives Ok/Err that inherit Result[T].
  • Because a union is a sealed hierarchy after desugaring, match exhaustiveness is checked exactly as in the previous section — a missing variant is a compile-time error.

The union form is deliberately data-only. When a variant needs its own contracts, invariants, or methods, write the explicit sealed deferred class form above; both compile to the same thing.

Enumerations (enum union). When every variant is payload-free, prefix the declaration with enum to get an enumeration — a closed set of named, ordered, canonical values:

enum union Color
  Red
  Green
  Blue
end

On top of the plain union desugaring (a sealed hierarchy plus match exhaustiveness), enum adds three things:

print(Color.Red == Color.Red)        -- true: interned, one canonical value
print(Color.Red < Color.Green)       -- true: ordered by declaration order
print(Color.values.length())         -- 3: an Array[Color] of every member
across Color.values as c do print(c.ordinal) end   -- 0  1  2
  • Members are interned constants on the type: Color.Red is the one Red, so == (reference identity) holds — no allocation per use.
  • Ordering comes from Comparable: members compare by the order they are written.
  • values is an Array[Color] of all members, in declaration order.

enum is allowed only when every variant is payload-free and the union is non-generic — its members are canonical constants, which need a concrete type. A variant may not be named values, ordinal, or compare (those back the generated members). Plain union never gains any of this; the enrichment is opt-in.

Scoped Blocks
let x: Integer := 10
do
  let x: Integer := 99          -- shadows the outer x
  print(x)                      -- 99
end
print(x)                        -- 10
Type Conversion: convert
convert <value> to <name>:<Type>
  • Returns true if conversion succeeds, else false.
  • On success, <name> is bound to the converted value.
  • On failure, <name> is bound to nil.
  • Conversion follows Java-style related-type rules: <Type> must be a supertype or subtype of the runtime type of <value>.
  • convert never changes a number's representation, so converting between Integer, Byte, Integer16, Integer32 and Real is rejected. Use to_byte(), to_integer16(), to_integer32(), to_integer() or to_real().
do
  convert vehicle_1 to my_car:Car
  -- my_car is visible in this block
end
if convert vehicle_1 to my_car:Car then
  my_car.sound_horn
end
Object Test: ?<expr> as <name>
?<expr> as <name>
  • <expr> must have a detachable (?T) type. Returns true if <expr> is non-nil, else false.
  • On success, <name> is bound to <expr>'s attached (non-detachable) value for the branch this guards.
  • Unlike x /= nil, <expr> can be any expression, not just a bare identifier — a dotted chain (?p.age as a) or a safe-navigation read (?p?.age as a) narrows directly, no intermediate let needed.
  • Chains of and are each narrowed in turn, so a later conjunct can use an earlier one's binding:
if ?p.age as a and ?q.age as b then
  print(when a < b then "p is younger" else "p is not younger" end)
end
Standard Result and Option

The standard library provides two sealed sum types for error handling and optional values, imported with intern:

intern data/Result
intern data/Option

let r: Result[Integer, String] := create Ok[Integer, String].make(10)
let doubled: Result[Integer, String] :=
  result_map(r, fn (x: Integer): Integer do result := x * 2 end)
print(doubled.unwrap_or(0))          -- 20

let o: Option[Integer] := create Some[Integer].make(7)
print(o.get_or(0))                   -- 7
  • Result[T, E] is Ok(value: T) or Err(error: E); Option[T] is Some(value: T) or None.
  • Query/unwrap are methods: is_ok(), is_err(), unwrap_or(fallback) on Result; is_some(), is_none(), get_or(fallback) on Option.
  • Transforming combinators are free functions (they introduce a fresh type parameter): result_map, result_and_then, result_map_err; option_map, option_and_then, option_filter. and_then is the bind that chains fallible steps and short-circuits on the first Err/None.
  • Construction infers type arguments from the constructor's arguments, so create Ok.make(10) gives Ok[Integer, Any] — assignable to Result[Integer, String] (a parameter the constructor does not mention, like Err's value type here, stays Any and acts as a wildcard). Write explicit arguments (create Ok[Integer, String].make(…)) when you need to pin them.
Match Statement

match dispatches on the runtime type of an expression. Used with a sealed parent class it becomes an exhaustive type switch — every variant must be handled or the typechecker rejects the program:

match r of
  Ok as ok then
    print(ok.value)
  Err as err then
    print(err.msg)
end
  • Each ClassName as var then clause binds var to the matched object, typed as ClassName. A clause's body is a single statement — use do ... end for more than one.
  • The typechecker verifies all sealed subclasses are covered. A missing variant is a compile-time error.
  • An else branch covers remaining cases and suppresses the exhaustiveness check:
match r of
  Ok as ok then
    print(ok.value)
  else
    print("not ok")
end

Destructuring and wildcards. A clause may destructure the matched variant's payload fields by name, instead of binding the whole object and reading .field:

match order of
  Draft                   then print("draft")
  Placed(id, total)       then print(id)        -- bind fields id, total
  Shipped(tracking as t)  then print(t)         -- rename tracking→t, ignore at
end
  • Variant(a, b) binds the payload fields named a and b to locals of the same name; Variant(a as x) binds field a to a local x. Order does not matter — fields are matched by name, and a field you do not name is ignored.
  • In a field pattern the name before the colon is always a field of the variant, and : means exactly one thing: this field has this type (total: Integer). Renaming is as. So Variant(a: x) does not bind x; it requires field a to be an x, and is an error unless x names a type. To compare a field to a value, use a guard.
  • as still binds the whole value and composes with destructuring (Placed(id, total) as p then …). A clause may bind neither (Draft).
  • _ is a catch-all, equivalent to else, and likewise suppresses the exhaustiveness check.

Destructuring and _ are pure sugar over the type-dispatch form above, so exhaustiveness and type checking behave identically (a destructured field the variant does not have is a compile-time error).

Guards. A clause may add an if <boolean> guard, evaluated after the type match and destructuring. A false guard falls through to the next clause:

match order of
  Placed(id, total) if total > 1000 then flag(id)
  Placed(id, total)                 then charge(id, total)
  Draft                             then note_draft()
end
  • The guard may reference the destructured fields and the as binding.
  • A guard must be Boolean.
  • A guarded clause does not count toward exhaustiveness (it might not fire), so a variant handled only by guarded clauses still needs an unguarded clause, a _, or else.

Guards run on the JVM (compiled) and interpreter backends.

Comparing a field to a value. Use a guard. There is no literal field pattern: Move(dx: 0) was once sugar for Move(dx) if dx == 0, and it is now rejected, with an error naming the guard to write.

match cmd of
  Move(dx, dy) if dx = 0 and dy = 0 then stay()
  Move(dx, dy)                      then move(dx, dy)
  Say(text) if text = "quit"        then bye()
  Say(text)                         then say(text)
end

The guard is not merely the surviving spelling — it is the better one. It binds the field it constrains (the literal form did not, so a body naming that field silently saw nil), and it leaves : with exactly one meaning in a field pattern: this field has this type.

Type patterns. A field pattern may pin a field to a type with field: Type; the clause matches only when the field is one at runtime, and binds the narrowed field under its own name:

match shape of
  Box(content: Circle) then print(content.radius)   -- content is a Circle here
  Box(content)         then print("not a circle")
end
  • Like a guard, a type pattern is a test, so a clause constrained by one does not count toward exhaustiveness. A test against the field's declared type is therefore a no-op that only costs you the exhaustiveness check — narrow a wider type (content: Any down to Circle); do not restate a known one.
  • The type may be a builtin (total: Integer), a user class, a parameterized type (items: Array[String]), or a declare type alias, which is tested as the type it names.
  • A refinement (declare type Quantity = Integer where n: n > 0) may not be used here, and is rejected. A type pattern is a runtime test, and a refinement's predicate is erased at runtime — the test could only check Integer and would match values Quantity excludes. Test the base type and put the predicate in a guard, or narrow with a typed let, which does run it.

Nested patterns. A type pattern may go on to match the narrowed field's payload — field: Type(sub-patterns):

match result of
  Ok(value: Some[Integer](value as x)) then use(x)   -- Ok whose value is a Some
  _                                    then fallback()
end
  • The field is narrowed with a runtime type test; if it is not that variant, the clause falls through (so, like a guard, a nested pattern does not count toward exhaustiveness).
  • With sub-patterns you reach the value through them, so the field itself is not bound; without them (inner: Some[Integer]) the narrowed field binds as inner.
  • Sub-patterns are matched by field name and nest arbitrarily deep. Give the nested type its arguments (Some[Integer]) for the bound sub-fields to keep their element type; without them the sub-fields bind as Any.
  • Match binding now carries the subject's generic arguments onto the clause variable, so a matched field such as o.inner has its real type (Option[Integer], not Option[Any]) — which is what lets the nested test type-check.

Nested patterns run on the JVM (compiled) and interpreter backends.

Anonymous Functions
let add: Function(a: Integer, b: Integer): Integer :=
  fn (a, b: Integer): Integer do result := a + b end
print(add(3, 4))                -- 7

Bare Function is still valid and compatible with any typed function value:

let f: Function := fn (n: Integer): Integer do result := n * 2 end

When the target already carries a concrete Function(...) type, fn's own parameter and return types can be left out — they're inferred from that target:

let add2: Function(a: Integer, b: Integer): Integer := fn(a, b) do result := a + b end
print(add2(3, 4))                -- 7

Inference only works where a target type is actually available (here, the let's own annotation). With no such context, every parameter needs its own explicit type, or it's a compile error.

A Function(...) type's parameter names are for documentation only — they can be dropped, leaving just the types:

let is_positive: Function(Integer): Boolean := fn(n: Integer): Boolean do result := n > 0 end
Type Aliases

declare type binds a name to any type expression — most commonly used to name a function signature for reuse:

declare type Transformer = Function(n: Integer): Integer

let double: Transformer := fn (n: Integer): Integer do result := n * 2 end
let square: Transformer := fn (n: Integer): Integer do result := n * n end

Any type can be aliased, not just function types:

declare type Matrix = Array[Array[Real]]
Refinement Types

A declare type with a where clause is a refinement type: an existing type narrowed by a boolean predicate, without declaring a class.

declare type Quantity   = Integer where n: n > 0
declare type Percentage = Real    where p: p >= 0.0 and p <= 100.0
declare type NonEmpty   = String  where s: s.length() > 0

where n: <expr> binds the value under test to n (any name) and gives a boolean predicate over it, mirroring the label: condition shape of contracts.

A refinement is its base type with a checked constraint — not a class: no fields, no constructor, no wrapper. A Quantity is an Integer at runtime, so it interoperates freely with its base:

let q: Quantity := 5          -- checked: raises if the value is not > 0
let total: Integer := q + 10  -- free: a Quantity is an Integer

The predicate is enforced wherever a value is narrowed into the refinement — a let of that type, a parameter of that type (checked at the call boundary), and a return of that type. Widening (Quantity → Integer) is always free.

function debit(amount: Quantity): Integer do   -- amount checked on entry
  result := amount
end

Notes and current limits:

  • Operations do not propagate the refinement: q + q is an Integer. Flow the result back into a refinement-typed binding to re-check it.
  • Fields, convert targets, ?R detachable bindings, and distinct nominal newtypes are not yet checked/supported — use a class where you need those.
  • The predicate should be side-effect free.
Generics
class Box [T]
  feature
    value: T
  create
    make(v: T) do value := v end
end

let b: Box [Integer] := create Box[Integer].make(42)
b.value                         -- 42

Generic functions use the same bracket syntax after the function name:

function first[T](values: Array[T]): T do
  result := values.get(0)
end

print(first([10, 20, 30]))      -- 10
print(first(["a", "b", "c"]))   -- "a"

Multiple generic parameters are allowed:

function pick_or_default[K, V](present: Boolean, value: V, fallback: V): V do
  result := when present then value else fallback end
end

Anonymous functions can also declare generic parameters explicitly:

let id: Function := fn[T](x: T): T do
  result := x
end

print(id(42))                   -- 42

Constrained Generics. T -> Bound constrains a generic parameter to a type, and the body may then use the routines and fields of that bound on a value of type T. The bound may be a builtin such as Comparable, or any class you declare:

deferred class Shape
  feature
    area(): Integer deferred
    name: String
end

function total_area[T -> Shape](xs: Array[T]): Integer do
  result := 0
  across xs as s do
    result := result + s.area()     -- a routine of the bound
  end
end

Calls through a bound dispatch dynamically, so a deferred routine resolves to the runtime subclass's override. The bound itself must be a plain type name: a parameterized bound ([T -> Addable[T]]) is not yet supported.

Assignment
x := 10                         -- set an existing variable
let y: Integer := 20            -- create a new variable
this.name := "Nex"              -- set a field inside a method
That's it!

Nex reads like English: if...then...end, from...until...do...end, repeat...do...end, across...as...do...end, class...feature...end. Write what you mean, and Nex will check that you mean what you write.