A compact, linear tour of Nex — from first programs to classes, contracts, and modules.
Nex programs are built from class declarations, function declarations, and calls.
print("Hello, Nex") -- "Hello, Nex"
Comments start with --:
-- This line is ignored by the compiler/interpreter
print("Hello again") -- "Hello again"
print shows strings with surrounding quotes and numbers as-is.
Variables are introduced with let and assigned with :=.
let name: String := "Ada"
let age: Integer := 12
let height: Real := 1.52
let ok: Boolean := true
In the REPL, type annotations are optional by default:
let x := 10
let y := x + 5
Built-in scalar types include:
IntegerByte (an unsigned 8-bit value, 0..255; see the scalar types reference)RealCharBooleanStringDetachable (nullable) types use a leading ?:
let maybe_name: ?String := nil
Arithmetic:
let a := 10 + 2 * 3 -- 16
let b := (10 + 2) * 3 -- 36
let c := 10 % 3 -- 1
let d := 2 ^ 8 -- 256
Comparison and boolean logic:
let e := a = b -- false
let f := a /= b -- true
let g := (a > 5) and (b < 40) -- true
let h := not g -- false
Operator precedence follows conventional order: unary, multiplicative, additive, comparison, equality, and, or.
if age >= 18 then
print("adult")
elseif age >= 13 then
print("teen")
else
print("child")
end
With age at 12 (from above), this prints "child".
when is an expression, so it can appear on the right-hand side:
let category := when age >= 18 then "adult" else "minor" end -- "minor"
case age of
0, 1, 2 then print("small")
3, 4, 5 then print("medium")
else print("large")
end
With age at 12, this prints "large".
Each then branch takes a statement. If you need multiple statements, use a scoped do ... end block.
from ... until ... do ... endfrom
let i: Integer := 1
until
i > 5
do
print(i)
i := i + 1
end
Prints 1 through 5, one per line.
repeatrepeat 3 do
print("tick")
end
Prints "tick" three times.
acrossacross [10, 20, 30] as x do
print(x)
end
Prints 10, 20, 30. across also works with strings and maps.
Top-level functions use function:
function greet(name: String)
do
print("Hello, " + name)
end
function double(n: Integer): Integer
do
result := n * 2
end
Call forms:
greet("Bob") -- "Hello, Bob"
print(double(5)) -- 10
Anonymous functions use fn:
let inc := fn (n: Integer): Integer do
result := n + 1
end
print(inc(10)) -- 11
let xs: Array [Integer] := [1, 2, 3]
print(xs.get(0)) -- 1
let m: Map [String, String] := {"name": "Nex", "kind": "language"}
print(m.get("name")) -- "Nex"
Array and map literals are expressions, so they can be nested.
let ids: Set[Integer] := #{1, 2, 3}
print(ids.contains(2)) -- true
print(ids.union(#{4})) -- #{1, 2, 3, 4}
An empty set uses #{}:
let empty_ids: Set[Integer] := #{}
spawn starts a lightweight concurrent task:
let t: Task[Integer] := spawn do
result := 40 + 2
end
print(t.await) -- 42
If a task does not produce a value, use plain Task:
let t: Task := spawn do
print("working") -- "working"
end
t.await
Tasks can also be timed or cancelled:
print(t.await(100))
print(t.cancel)
print(t.is_cancelled)
Task groups can be joined in two ways:
print(await_any([t1, t2]))
print(await_all([t1, t2]))
Channels let tasks exchange values safely:
let ch: Channel[Integer] := create Channel[Integer]
spawn do
ch.send(42)
end
print(ch.receive) -- 42
ch.close
By default, channels are unbuffered: send and receive rendezvous, so each side waits for the other.
For buffered communication:
let ch: Channel[Integer] := create Channel[Integer].with_capacity(2)
ch.send(1)
ch.send(2)
print(ch.size) -- 2
Non-blocking probes:
print(ch.try_send(3))
print(ch.try_receive)
Timed channel operations:
print(ch.send(3, 50))
print(ch.receive(50))
Use select when you want to react to whichever channel operation is ready first, or to a task that has already finished:
select
when inbox.receive as msg then
print(msg)
when worker.await as value then
print(value)
when control.receive as signal then
print(signal)
timeout 100 then
print("timed out")
else
print("idle")
end
select uses channel readiness checks for channels and is_done checks for tasks. Task clauses use Task.await, but only become selectable after the task has already completed. If no clause is ready and there is no else, select waits until one becomes ready. If a timeout clause is present, that body runs once the timeout expires.
For the precise concurrency semantics and runtime design, see the Concurrency Guide.
A class groups fields and methods.
class Counter
create
make(start: Integer) do
this.value := start
end
feature
value: Integer
inc() do
this.value := this.value + 1
end
current(): Integer do
result := value
end
end
Construction uses create:
let c: Counter := create Counter.make(10)
c.inc
print(c.current) -- 11
Parameterless calls may omit parentheses (c.inc, c.current).
class Box [T]
create
make(initial: T) do
this.value := initial
end
feature
value: T
get(): T do
result := value
end
end
With constraints:
class Dictionary [K -> Hashable, V]
feature
key: K
value: V
end
class Animal
feature
name: String
speak do
print(name)
end
create
named(name: String) do
this.name := name
end
end
class Dog
inherit Animal
feature
speak do
print(name + " says woof")
end
end
let a: Animal := create Animal.named("Ko")
a.speak -- "Ko"
let d: Animal := create Dog.named("Ki")
d.speak -- "Ki says woof"
Contracts are first-class in Nex.
class Wallet
feature
money: Real
spend(amount: Real)
require
non_negative_amount: amount >= 0.0
enough: amount <= money
do
this.money := money - amount
ensure
decreased: money = old money - amount
end
create
with_balance(amount: Real) do
money := amount
end
invariant
never_negative: money >= 0.0
end
let w: Wallet := create Wallet.with_balance(-10.0)
Error: Class invariant violation: never_negative
With a valid balance the methods run, and a broken precondition is reported the same way:
let w: Wallet := create Wallet.with_balance(10.2)
w.spend(9.0)
w.money -- 1.1999999999999993
w.spend(2.0) -- Error: Precondition violation: enough
Use contracts to state assumptions (require), guarantees (ensure), and global consistency rules (invariant).
Those three speak about a routine's boundaries or a whole class. To state something that must hold part way through a body, use assert:
assert enough_left: money >= 0.0 -- named: the failure reports the name
assert money >= 0.0 -- bare: the failure reports the line
A failed assert reports a contract violation exactly as require and ensure do, which makes it the natural way to write checks in a test routine:
function test_spend() do
let w: Wallet := create Wallet.with_balance(10.0)
w.spend(4.0)
assert spent: w.money = 6.0
end
let attempts := 0
do
attempts := attempts + 1
if attempts < 3 then
raise "not ready"
end
print("ok")
rescue
print("retrying")
retry
end
raise, rescue, and retry provide structured recovery paths.
import java.util.Scanner
intern math/Calculator
intern math/Calculator as Calc
intern first searches relative to the loaded file, then the current working directory, then ~/.nex/deps. For path-qualified classes it accepts lib/<path>/<Class>.nex, lib/<path>/<class>.nex, and the corresponding src/ variants.
Use intern for Nex-to-Nex modularity and import for target-platform interop.
class BankAccount
create
make(initial: Real) do
this.balance := initial
end
feature
balance: Real
deposit(amount: Real)
require
positive: amount > 0.0
do
this.balance := balance + amount
ensure
grew: balance >= old balance
end
withdraw(amount: Real)
require
positive: amount > 0.0
enough: amount <= balance
do
this.balance := balance - amount
ensure
shrank: balance = old balance - amount
end
show do
print("balance = " + balance)
end
invariant
never_negative: balance >= 0.0
end
let account := create BankAccount.make(100.0)
account.deposit(25.0)
account.withdraw(40.0)
account.show -- "balance = 85.0"
intern (Section 13).Ready to go deeper? The book Programming with Nex covers everything here at a teaching pace — with the reasoning behind each feature, larger worked examples, and graded exercises.
max(a, b: Integer): Integer using if.10 down to 1.Pair [A, B] class with first and second.transfer(amount) method between two accounts.intern.Optional tooling — skip on a first read. Nex ships an interactive debugger you can drive from the REPL once you are comfortable writing programs. This is a quick reference; the full command set is in docs/md/DEBUGGER.md.
Enable the debugger in the REPL:
:debug on
Set breakpoints and run:
:break Wallet.spend
:break Wallet.spend if amount > 100
:break field:money
:tbreak Wallet.spend:42
At the dbg> prompt:
:where
:locals
:print money
:next
:continue
Watch values change:
:watch money
:watch money if money > 100
:watches
Tune breakpoint hit behavior:
:ignore 1 2 -- breakpoint[1] ignores first 2 hits
:every 1 3 -- breakpoint[1] pauses every 3rd hit
Control breakpoints without deleting:
:disable 1
:enable 1
Pause on failures:
:breakon exception on
:breakon contract on
:breakon contract filter invariant
Save and restore debugger state:
:breaksave .nex-debug.edn
:breakload .nex-debug.edn