This tutorial is a compact, linear introduction to Nex in small steps, with complete examples.
Unless noted otherwise, snippets are REPL-friendly fragments.
Nex programs are built from class declarations, function declarations, and calls.
print("Hello, Nex")
Comments start with --:
-- This line is ignored by the compiler/interpreter
print("Hello again")
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:
IntegerInteger64RealDecimalCharBooleanStringDetachable (nullable) types use a leading ?:
let maybe_name: ?String := nil
Arithmetic:
let a := 10 + 2 * 3
let b := (10 + 2) * 3
let c := 10 % 3
let d := 2 ^ 8
Comparison and boolean logic:
let e := a = b
let f := a /= b
let g := (a > 5) and (b < 40)
let h := not g
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
when is an expression, so it can appear on the
right-hand side:
let category := when age >= 18 "adult" else "minor" end
case age of
0, 1, 2 then print("small")
3, 4, 5 then print("medium")
else print("large")
end
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
repeatrepeat 3 do
print("tick")
end
acrossacross [10, 20, 30] as x do
print(x)
end
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")
print(double(5))
Anonymous functions use fn:
let inc := fn (n: Integer): Integer do
result := n + 1
end
print(inc(10))
let xs: Array [Integer] := [1, 2, 3]
print(xs.get(0))
let m: Map [String, String] := {"name": "Nex", "kind": "language"}
print(m.get("name"))
Array and map literals are expressions, so they can be nested.
let ids: Set[Integer] := #{1, 2, 3}
print(ids.contains(2))
print(ids.union(#{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)
If a task does not produce a value, use plain Task:
let t: Task := spawn do
print("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)
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)
Non-blocking probes are also available:
print(ch.try_send(3))
print(ch.try_receive)
Timed channel operations use the same feature names with an extra timeout argument:
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.
On the JavaScript target, the source syntax is unchanged, but
generated code uses async/await under the hood. That means
spawn, Task.await, Channel.send,
and Channel.receive are lowered to Promise-based operations
in generated JavaScript.
For the precise concurrency semantics, target differences, and runtime design, see CONCURRENCY.md.
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)
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: A := create Animal.named("Ko")
a.speak -- "Ko"
let d: Animal := create Dog.named("Ki")
d.speak -- "Ki says woof"
Nex supports inheritance with inherit for code reuse and
specialization.
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)
Error: Class invariant violation: never_negative
Enable debugger in REPL:
:debug on
Set breakpoints and run:
:break Wallet.spend
:break Wallet.spend if amount > 100
:break field:money
:tbreak Wallet.spend:42
At 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
Script debugger commands from a file:
:debugscript debug_commands.dbg
For full command reference, see docs/md/DEBUGGER.md.
let w: Wallet := create Wallet.with_balance(10.2)
w.spend(9)
w.money -- 1.1999999999999993
w.spend(2) -- Error: Precondition violation: enough
Use contracts to state assumptions (require), guarantees
(ensure), and global consistency rules
(invariant).
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
import Math from './math.js'
intern math/Calculator
intern math/Calculator as Calc
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
intern (Section 13).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.