// tutorial

A Tour of Nex

A compact, linear walk through the language — from first programs to classes, contracts, and modules.

On this page
Already program? This is the fast tour. If you are new to programming — or want these ideas taught in depth, with motivation, worked examples, and exercises — read the book Programming with Nex instead.

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:

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

  1. Write and run tiny programs from Sections 1–5.
  2. Implement 3–5 utility functions (Section 6).
  3. Build one small class with constructor and contracts (Sections 8 and 11).
  4. Add one inheritance step (Section 10).
  5. 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

  1. Implement max(a, b: Integer): Integer using if.
  2. Write a loop that prints numbers 10 down to 1.
  3. Define a generic Pair [A, B] class with first and second.
  4. Add contracts to a transfer(amount) method between two accounts.
  5. 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.