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: 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.

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

Nex has two kinds of equality:

  • = and /= compare by value
  • == and != compare by identity
print([1, 2] = [1, 2])           -- true
print("abc" = "abc")             -- true

let a := [1, 2]
let b := a
let c := [1, 2]
print(a == b)                    -- true
print(a == c)                    -- false
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 "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 [key, value] pairs:

across {"name": "Alice", "age": "10"} as pair do
  print(pair.get(0))
end
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

For mutually recursive functions, declare the signatures first:

function is_even(n: Integer): Boolean
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

No default arguments. Nex has no default parameter values; a call must pass exactly as many arguments as the function 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. Class methods, however, can be overloaded by arity (see Optional arguments via method overloading), which is the idiomatic way to get optional-argument ergonomics.

Arrays
let colors: Array [String] := ["red", "green", "blue"]
print(colors.get(0))            -- "red"
colors.add("yellow")            -- add to the end
print(colors.length)            -- 4
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] := #{}

Build a set from an array (duplicates are removed):

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

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

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

Buffered channels:

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

Use select to wait on multiple operations:

select
  when jobs.receive as job then
    print(job)
  when worker.await as value then
    print(value)
  timeout 1000 then
    print("timed out")
  else
    print("idle")
end
Classes
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
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
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
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
Once Fields

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

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

Assigning a once field outside a constructor is a compile-time error.

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.AREA)               -- 2073600

Because an object- or collection-valued constant is one shared value, C.x == C.x holds for it. A forward or cyclic reference among constants 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
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. 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 deferred class closes its hierarchy — only its declared subclasses may extend it.

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)

A union is concise sugar for a sealed hierarchy of data variants: it expands to a sealed deferred class parent and one class per variant, with the payload as fields and an auto-generated make.

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

Construction and match (with exhaustiveness) are the same as for the hand-written form. A payload-free variant still gets a nullary make, and generic parameters carry through (union Result[T]).

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

enum union Color
  Red
  Green
  Blue
end

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

enum is allowed only when every variant is payload-free and the union is non-generic; a variant may not be named values, ordinal, or compare. Plain union never gains these — 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.
if convert vehicle_1 to my_car:Car then
  my_car.sound_horn
end
Match Statement

Dispatches on the runtime type of an expression. Against a sealed type, the typechecker verifies every variant is covered — a missing branch is a compile-time error.

match r of
  when Ok as ok then
    print(ok.value)
  when Err as err then
    print(err.msg)
end

An else branch covers remaining cases and suppresses the exhaustiveness check. when _ is a catch-all and does the same.

match r of
  when Ok as ok then
    print(ok.value)
  else
    print("not ok")
end

Instead of binding the whole value with as and reading .field, a clause can destructure the variant's payload by field name. Order does not matter, and a field you do not name is ignored.

match r of
  when Ok(value) then print(value)
  when Err(msg)  then print(msg)
end

Inside a pattern the name to the left of a colon is always a field of the variant. : always constrains that field, and as always renames it. So Ok(value as v) binds the field value to a local v — while Ok(value: v) would mean "field value must be a v", and is an error unless v names a type.

match r of
  when Ok(value as v) then print(v)
  when Err(msg as m)  then print(m)
end

A field can be constrained to a type. A type pattern narrows the field and binds it under its own name, so the branch can use it at that type:

match c of
  when Boxed(content: Integer) then print(content + 1)     -- content is an Integer here
  when Boxed(content: String)  then print(content.length)
  when Boxed(content)          then print("something else")
  when Empty                   then print("empty")
end

To compare a field to a value, add an if guard — evaluated after the type match and destructuring, with the destructured names in scope. A false guard falls through to the next clause:

match r of
  when Ok(value) if value = 10  then print("exactly ten")
  when Ok(value) if value > 100 then print("big")
  when Ok(value)                then print(value)
  when _                        then print("other")
end

There is no literal field pattern: Ok(value: 10) is rejected, and the error names the guard to write instead.

A type pattern is a runtime test, not an annotation. Testing a field against the type it is already declared to have does nothing except forfeit the exhaustiveness check — narrow a wider type, do not restate a known one. For the same reason a refinement (declare type Quantity = Integer where n: n > 0) cannot be used as a type pattern and is rejected: its predicate is erased at runtime, so the test could only check Integer and would match values Quantity excludes. Test the base type and put the predicate in a guard.

A type pattern may go on to match the narrowed field's own payload, nesting arbitrarily deep. Give the nested type its arguments (Some[Integer]) for the bound sub-fields to keep their element type; without them they bind as Any.

match r of
  when Ok(inner: Some[Integer](value as x)) then use(x)   -- an Ok whose inner is a Some
  when _                                    then fallback()
end

Literals, types, nested patterns and guards are all tests — a clause constrained by one might not fire, so it does not count toward exhaustiveness. A variant handled only by such clauses still needs an unguarded clause, a when _, or an else.

Anonymous Functions
let add: Function := fn (a, b: Integer): Integer do result := a + b end
print(add(3, 4))                -- 7
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:

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"
Assignment
x := 10                         -- set an existing variable
let y: Integer := 20            -- create a new variable
this.name := "Nex"              -- set a field inside a method