1. A First Program
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.
2. Values, Variables, and Types
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)RealCharBooleanString
Detachable (nullable) types use a leading ?:
let maybe_name: ?String := nil
3. Expressions and Operators
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.
4. Control Flow
4.1 Conditional execution
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".
4.2 Expression-level choice
when is an expression, so it can appear on the right-hand side:
let category := when age >= 18 then "adult" else "minor" end -- "minor"
4.3 Case analysis
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.
5. Repetition
5.1 from ... until ... do ... end
from
let i: Integer := 1
until
i > 5
do
print(i)
i := i + 1
end
Prints 1 through 5, one per line.
5.2 repeat
repeat 3 do
print("tick")
end
Prints "tick" three times.
5.3 across
across [10, 20, 30] as x do
print(x)
end
Prints 10, 20, 30. across also works with strings, maps and anything that exposes a cursor.
6. Functions
Top-level functions:
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
7. Data Structures
7.1 Arrays
let xs: Array [Integer] := [1, 2, 3]
print(xs.get(0)) -- 1
7.2 Maps
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.
7.3 Sets
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] := #{}
7.4 Tasks and Channels
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.
8. Classes and Objects
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).
9. Generic Classes
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
10. Inheritance
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"
11. Design by Contract
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
12. Errors and Recovery
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.
13. Modules and Interop
13.1 Import external Java symbols
import java.util.Scanner
13.2 Load Nex classes from other files
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.
14. A Complete Example
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"
15. Suggested Study Order
- Write and run tiny programs from Sections 1–5.
- Implement 3–5 utility functions (Section 6).
- Build one small class with constructor and contracts (Sections 8 and 11).
- Add one inheritance step (Section 10).
- Split code into files and use
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.
16. Exercises
- Implement
max(a, b: Integer): Integerusingif. - Write a loop that prints numbers
10down to1. - Define a generic
Pair [A, B]class withfirstandsecond. - Add contracts to a
transfer(amount)method between two accounts. - Create two files and load one class from the other with
intern.
17. Debugger Quickstart
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; type :help at the nex> or dbg> prompt for every command.
Enable the debugger in the REPL:
:debug on
Turn it on before you define or :load the code you want to debug. Classes defined while the debugger is off run compiled and do not stop at breakpoints.
Set breakpoints, then run some code (for example w.spend(150.0) with the Wallet from Section 11):
:break Wallet.spend
:break Wallet.spend if amount > 100
:break field:money
:tbreak Wallet.spend
:breaks
field:money pauses on writes of the form this.money := .... :tbreak sets a temporary breakpoint that is removed after its first hit.
At the dbg> prompt:
:where
:locals
:print money
:step -- or :s, step into calls
:next -- or :n, step over calls
:finish -- or :f, run until the current method returns
:frames
:frame 1
:continue -- or :c
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
Hit counts and watched values start fresh with each REPL input, so :ignore, :every, and watchpoints act within a single input — for example a loop:
across [1, 2, 3, 4, 5, 6] as x do w.spend(1.0) end
Control breakpoints without deleting:
:disable 1
:enable 1
Remove them:
:clearbreak 1
:clearbreak all
:clearwatch all
Pause on failures:
:breakon exception on
:breakon contract on
:breakon contract filter invariant
The contract filter takes pre, post, invariant, or assert — or any substring to match against the violation message.
Save and restore debugger state:
:breaksave .nex-debug.edn
:breakload .nex-debug.edn
Script debugger commands from a file:
:debugscript debug_commands.dbg
The file's lines are fed, in order, to the dbg> prompt whenever execution pauses. Turn the debugger off with :debug off.