Every construct in Nex — one card at a time.
-- This is a comment. The computer ignores it.
-- Use comments to explain your code to other humans!
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.
print("Hello, world!")
print(name)
print("I am " + age + " years old")
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.
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 identityprint([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
if age >= 18 then
print("You can vote!")
elseif age >= 13 then
print("You are a teenager")
else
print("You are a kid")
end
let label: String := when age >= 18 "adult" else "minor" end
from let i: Integer := 1 until i > 5 do
print(i)
i := i + 1
end
-- prints 1 2 3 4 5
repeat 3 do
print("hello!")
end
-- prints hello! three times
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
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.
let colors: Array [String] := ["red", "green", "blue"]
print(colors.get(0)) -- "red"
colors.add("yellow") -- add to the end
print(colors.length) -- 4
let pet: Map [String, String] := {"name": "Max", "kind": "dog"}
print(pet.get("name")) -- "Max"
pet.put("kind", "cat") -- update a value
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}
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
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
let cat: Pet := create Pet.make("Mimi", "meow")
cat.speak -- "Mimi says meow"
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
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.)
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
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.
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.
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
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 direction of
"up" then print("going up")
"down" then print("going down")
else print("standing still")
end
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
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.
let x: Integer := 10
do
let x: Integer := 99 -- shadows the outer x
print(x) -- 99
end
print(x) -- 10
convert <value> to <name>:<Type>
true if conversion succeeds, else false.<name> is bound to the converted value.<name> is bound to nil.if convert vehicle_1 to my_car:Car then
my_car.sound_horn
end
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.
let add: Function := fn (a, b: Integer): Integer do result := a + b end
print(add(3, 4)) -- 7
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"
x := 10 -- set an existing variable
let y: Integer := 20 -- create a new variable
this.name := "Nex" -- set a field inside a method