Nex

Tour

Every language feature in small, focused snippets — literals, bindings, blocks, arithmetic, arrays, structs, lambdas, functions, modes, closures, control flow, prelude, testing, modules.

Comments

// Line comment.

/* Block comment. */

/* Block comments /* can nest */ — convenient for commenting out code. */

Layout: separators and line continuation

val a = 1; val b = 2        // multiple statements on one line, separated by ;

val long_expression =       // a line ending with an operator or open
  some_function(x, y)       // delimiter continues on the next (more-
    + another(z)            // indented) line; no backslash needed
    * yet_another(w)

; is rarely used; line continuation is common for expression-heavy numerical code.

Literals

Booleans, integers, reals

val flag    = true
val n       = 42
val big     = 1_000_000
val hex     = 0xFF              // 255
val bin     = 0b1010_1010       // 170
val oct     = 0o755             // 493
val pi_ish  = 3.14159
val sci     = 6.022e23
val sep     = 1_234.567_89

Strings and escapes

val hello   = "hello, world"
val multi   = "line one\nline two"
val tabbed  = "col1\tcol2"
val quoted  = "she said \"hi\""
val unicode = "\u{1F600}"           // 😀
val byte    = "\xC3\xA9"             // bytes for "é"

Interpolated strings (s"...")

val name = "Ada"
val n = 42
val z = 3.0 + 4i

print(s"hello, $name")                  // hello, Ada
print(s"n = $n, n+1 = ${n + 1}")        // n = 42, n+1 = 43
print(s"|z| = ${z.abs()}")              // |z| = 5.0
print(s"first element = ${[10, 20][0]}")
print(s"literal dollar: $$")            // literal dollar: $

// $ident.field treats only $ident as the splice — .field is literal text.
// Use ${ident.field} to interpolate a field access:
print(s"$z.re")                         // (3.0 + 4.0i).re
print(s"${z.re}")                       // 3.0

Complex via the i constant

val a = i                   // (0.0 + 1.0i)  — imaginary unit
val b = 2i                  // (0.0 + 2.0i)  — juxtaposition: 2 * i
val c = 3 + 4i              // (3.0 + 4.0i)
val d = exp(pi * i)         // ~ (-1.0 + 0.0i)  (Euler's identity)

i is the prelude name; for i in 0..n shadows it normally. Pick a different index name (j, k, n, m) if you need both in the same scope.

Unit

val nothing = ()            // the single value of type `unit`

Bindings

val x = 10                  // immutable + has storage
var y = 20                  // mutable + has storage
y = 30                      // OK
y += 1                      // OK — compound assignment, same as y = y + 1
                            // (also: -=  *=  /=  %=)

val n: integer = 7          // explicit type annotation
var arr: [real] = [1.0, 2.0, 3.0]
val empty: [real] = []      // empty literal needs annotation

// `const` — compile-time constant; RHS must be a constant expression.
// May appear at any scope; compiler may inline at use sites.
const PI_OVER_2 = pi / 2
const TOL       = 1.0e-12
const MAX_ITER  = 1000

// `const` inside a function works too:
def newton(f: real -> real, x0: real) =
  const LOCAL_TOL = 1.0e-8
  // ...

// Calls to pure functions (prelude or user-defined) returning a scalar
// are constant expressions:
const SQRT_2 = sqrt(2.0)            // OK — sqrt is pure
const HYP    = hypot(3.0, 4.0)      // OK — 5.0

def square(x: real): real = x * x
const NINE   = square(3.0)          // OK — square is pure

// Calls that return non-scalars (arrays, tuples, structs) still need val:
val SAMPLES = linspace(0.0, 1.0, 100)

// Shadowing in a new scope is allowed:
val p = 1
if true then
  val p = 99                // OK: new scope shadows outer p
  print(p)                  // 99
end if
print(p)                    // 1

Block expressions

A multi-statement block is itself an expression — the value of the block is its final expression. Block expressions arise wherever an expression is expected: val RHS, function bodies, lambda bodies, branches of if.

val computed =
  val temp   = sqrt(2.0)
  val scaled = temp * 100.0
  scaled + 1.0             // value of the block is this final expression

// `if` branches are block expressions too:
val classified =
  if x > 0.0 then
    val msg = "positive"
    print(s"got $msg")
    msg
  else
    val msg = "non-positive"
    print(s"got $msg")
    msg

Arithmetic

7 + 3        // 10
7 - 3        // 4
7 * 3        // 21
7 / 3        // 2.333...  (real division — operands promoted)
7 div 3      // 2        (integer division — `div` keyword)
7 % 3        // 1        (modulo, integer)
2 ^ 10       // 1024     (exponentiation)
2.0 ^ 0.5    // 1.414...
-7           // unary minus

Numeric promotion

1 + 2.0      // integer 1 → real 1.0; result real 3.0
1.0 + 2i     // real → complex; result (1.0 + 2.0i)
1 + 2i       // integer → real → complex; result (1.0 + 2.0i)

Comparison and logical

5 < 10           // true
5 == 5           // true
5 != 5           // false

true and false   // false
true or false    // true
not true         // false

// Element-wise on arrays:
val v = [1, 2, 3, 4]
val mask = v < 3                          // [true, true, false, false]

// Chained comparisons read like the math — `0 <= i < n` desugars to
// `(0 <= i) and (i < n)` with the middle operand evaluated at most
// once and only when the previous compare held:
val i = 3
val n = 10
val in_range = 0 <= i < n                 // true

// Any number of comparisons may be chained; operators can be mixed.
val tight = 1 < i <= 3 < n                // true

Juxtaposition multiplication

val x = 5.0
val a = 2x                  // 2 * x = 10.0
val b = 2(x + 1)            // 2 * (x + 1) = 12.0
val c = 2pi                 // 2 * pi
val d = 3sin(pi/4)          // 3 * sin(pi/4)
val e = 2i                  // 2 * i

// Identifier-prefix is NOT juxtaposition — xy is one identifier.
val y = 4.0
val p = x * y               // explicit * required between identifiers

// Precedence: ^ > juxtaposition > * / // %
val q = 2x^3                // 2 * (x^3) = 250.0
val r = 1/2x                // 1 / (2 * x) = 0.1
val s = 2x * 3              // (2x) * 3 = 30.0

Arrays — rank-1

val v: [real] = [1.0, 2.0, 3.0]

length(v)              // 3
v[0]                   // 1.0
v[-1]                  // 3.0             — negative indices wrap from the end
v[1..3]                // [2.0, 3.0]      slice (owned copy)
v[0..=1]               // [1.0, 2.0]      inclusive slice
v[-2..]                // [2.0, 3.0]      open upper bound — fills with length(v)
v[..2]                 // [1.0, 2.0]      open lower bound — fills with 0
v[..]                  // [1.0, 2.0, 3.0] both ends open — whole-array copy

val w = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0]
w[0..8 by 2]           // [10.0, 30.0, 50.0, 70.0]   — strided: every k-th element
w[1..8 by 3]           // [20.0, 50.0, 80.0]
w[.. by 2]             // [10.0, 30.0, 50.0, 70.0]   — stride combines with open bounds
w[-4.. by 2]           // [50.0, 70.0]                 and with negative bounds

// Element-wise arithmetic with broadcasting:
val a = [1.0, 2.0, 3.0]
val b = [4.0, 5.0, 6.0]
val c = 2a + b - 1.0   // [6.0, 9.0, 12.0]  — fuses to one loop

// View-style slicing — non-copying borrow into the source's buffer.
// Reads and writes alias the source; every rank-1 prelude function
// (sum, length, map, dot, ...) accepts a view transparently.
var src = [10, 20, 30, 40, 50]
val win = src.view(1..4)             // borrows src[1..4] — [20, 30, 40]
win[0] = 99                          // writes through: src is [10, 99, 30, 40, 50]
sum(win)                             // 99 + 30 + 40 = 169

// 3-arg form: strided view — borrow every k-th element of the window.
val big   = [10, 20, 30, 40, 50, 60, 70, 80]
val every2 = big.view(0..8, 2)       // [10, 30, 50, 70]
val every3 = big.view(1..8, 3)       // [20, 50, 80]
// View-of-view composes strides: this borrows every 4th of `big`.
every2.view(0..4, 2)                 // [10, 50]

Arrays — rank-2 (matrices)

val M: [[real]] = [[1.0, 2.0, 3.0],
                   [4.0, 5.0, 6.0]]        // 2×3

rows(M)                // 2
cols(M)                // 3
shape(M)               // (2, 3)
M[1, 2]                // 6.0               — element access
M[0]                   // [1.0, 2.0, 3.0]   — i-th row
M[:, 1]                // [2.0, 5.0]        — j-th column
M[0, 0..2]             // [1.0, 2.0]        — row 0, cols 0-1
M[0..2, 1..3]          // [[2.0, 3.0], [5.0, 6.0]]  — submatrix
M[1.., :]              // [[4.0, 5.0, 6.0]]         — open lo: rows 1..rows(M)
M[..2, ..2]            // [[1.0, 2.0], [4.0, 5.0]]  — open hi on both axes

// Element-wise on rank-2:
val N = [[10.0, 20.0, 30.0], [40.0, 50.0, 60.0]]
val P = M + N          // [[11.0, 22.0, 33.0], [44.0, 55.0, 66.0]]
val Q = 2M             // scalar broadcasts

// Matrix multiplication via @ (not element-wise):
val A = [[1.0, 2.0], [3.0, 4.0]]    // 2×2
val u = [5.0, 6.0]
val Au = A @ u                       // [17.0, 39.0]   matrix-vector
val AA = A @ A                       // [[7.0, 10.0], [15.0, 22.0]]  matrix-matrix
val d  = u @ u                       // 61.0           dot (rank-1 @ rank-1)

// Rank-2 view: borrow a contiguous row range. Row-major layout keeps
// the window contiguous, so no stride field is needed.
var grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
val band = grid.view(1..3)           // rows 1..3 — a 2×3 view
band[0, 1] = 99                      // writes through: grid[1, 1] is now 99

// 3-arg form: sub-rectangle view — borrow a window in both axes. The
// visible rows have gaps in the underlying buffer; the descriptor
// carries a row-stride field so flat iteration (sum, map, transpose)
// still picks up the right elements.
val sub = grid.view(1..3, 0..2)      // [[4, 5], [99, 8]]    — 2×2 sub-matrix

[byte] buffers

A packed byte buffer for file I/O and image processing. Bytes are storage — indexing widens to an integer in 0..255, and writes outside that range trap.

val b = bytes(4)                // zero-filled buffer, length 4
b[0] = 0xFF                     // each cell holds 0..255
b[1] = 0x80
b[2] = 0x00
b[3] = 0x42

print(length(b))                // 4
print(b[0])                     // 255       — read widens to integer
print(b)                        // [0xFF, 0x80, 0x00, 0x42]

// Widen, compute, narrow:
val ints = to_integers(b)       // [255, 128, 0, 66]            : [integer]
val out  = to_bytes(ints)       // back to [byte] — traps if any > 255

// File I/O works on every backend (the prelude routes through
// the cross-platform runtime, so the same program runs on JVM,
// Node, and native).
write_bytes("hello.bin", b)
val read_back = read_bytes("hello.bin")
print(read_back == b)           // true

There is no scalar byte type — byte only exists as the element of a [byte] buffer, and arithmetic on bytes is always done by widening to integer. There is no element-wise arithmetic, broadcasting, or view form for [byte]; slicing copies. The full design is in the Specification, §3.3.1 and §10.9.

Slice assignment — Fortran-90 array sections

A slice expression on the left of = overwrites the corresponding sub-extent of a var array in place. The buffer is mutated, not reallocated.

var xs = [10, 20, 30, 40, 50]
xs[1..4] = [200, 300, 400]               // xs = [10, 200, 300, 400, 50]
xs[0..=2] = [1, 2, 3]                    // xs = [1, 2, 3, 400, 50]
xs[-2..]  = [99, 100]                    // open hi: xs = [1, 2, 3, 99, 100]

var ys = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
ys[0..10 by 2] = [1, 1, 1, 1, 1]         // strided: ys = [1,0,1,0,1,0,1,0,1,0]

var m = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
m[1, :]       = [40, 50, 60]             // replace row 1
m[:, 0]       = [-1, -4, -7]             // replace column 0
m[0..2, 0..2] = [[10, 20], [30, 40]]     // replace a 2×2 sub-matrix

The right-hand side must be shape-conforming with the slice; mismatches trap at runtime. See the Slice assignment section of the Expressions chapter for the full rules.

Tuples

val t = 1, 2.0, "three"                     // no parens needed

t.0                    // 1
t.1                    // 2.0
t.2                    // "three"

// Destructuring at binding — no parens needed
val a, b, c = t
val x, _ = 3.14, "ignored"

// Parens are only needed in contexts where commas mean something else,
// e.g. function arguments or array elements:
f(1, 2, 3)             // 3 separate arguments
f((1, 2, 3))           // 1 argument, the tuple
[(1, 2), (3, 4)]       // array of 2 tuples

Ranges

0..5                   // half-open: 0, 1, 2, 3, 4
0..=5                  // inclusive: 0, 1, 2, 3, 4, 5

for i in 0..3 do
  print(i)             // prints 0, 1, 2
end for

val nums = range(0, 10)         // materialize range to [integer]

Structs

struct Point
  x: real
  y: real
end

val p = Point(3.0, 4.0)
p.x                    // 3.0
p.y                    // 4.0

// Mutate via var binding:
var q = Point(0.0, 0.0)
q.x = 5.0              // OK because q is var
q = Point(1.0, 1.0)    // rebind whole value

Sum types and match

enum Shape =
  Circle(radius: real)
  Rect(width: real, height: real)
  Empty                                       // bare variant — no fields

def area(s: Shape) =
  s match
    Circle(r)  -> pi * r^2
    Rect(w, h) -> w * h
    Empty      -> 0.0

print(area(Circle(2.0)))     // 12.566370614359172
print(area(Rect(3.0, 4.0)))  // 12.0
print(area(Empty))           // 0.0

Bare variants like Empty are values; fielded variants like Circle(radius) are constructors. The scrutinee sits to the left of match and arms use Pattern -> body; an optional end match may close the block. match is exhaustive — every variant of the scrutinee’s enum must have an arm (or you opt out explicitly with _ ->); field patterns bind by declaration order, and _ discards.

Lambdas

val sq      = (x: real) -> x * x         // explicit param type
val sq2     = x -> x * x                  // type inferred from context
val sum_xy  = (x, y) -> x + y             // multi-arg
val biggy   = x ->                        // block-form
  val y = x * x
  y + 1

Functions

// Single-expression form
def double(x: real) = 2x

// Block-body form
def normalize(v: [real]) =
  val m = sqrt(dot(v, v))
  v / m

// Explicit return type (required for recursive functions)
def factorial(n: integer): integer =
  if n <= 1 then 1
  else n * factorial(n - 1)

// Mutual recursion
def is_even(n: integer): bool =
  if n == 0 then true else is_odd(n - 1)

def is_odd(n: integer): bool =
  if n == 0 then false else is_even(n - 1)

// Early return
def first_positive(v: [real]) =
  for x in v do
    if x > 0.0 then return x
  end for
  0.0           // fallback if nothing matched

// Unit-returning function
def greet(name: string) =
  print(s"hello, $name")
  ()

// Default parameter values + named arguments — defaults sit on the
// trailing positions; callers can pass `name = value` in any order
// for any parameter after the positional run.
def make_box(w: real, h: real = 1.0, depth: real = 1.0) =
  w, h, depth

make_box(3.0)                            // (3.0, 1.0, 1.0)
make_box(3.0, 5.0)                       // (3.0, 5.0, 1.0)
make_box(3.0, depth = 7.0)               // (3.0, 1.0, 7.0)
make_box(w = 2.0, depth = 4.0, h = 3.0)  // (2.0, 3.0, 4.0)

Parameter modes

// `read` mode is INFERRED when the body doesn't mutate the parameter.
// The function reads but never modifies v:
def sum_squares(v: [real]) =                  // inferred read
  var s = 0.0
  for x in v do s = s + x*x end for
  s

// `mut` mode is DECLARED when the body mutates the parameter:
def scale_in_place(v: mut [real], factor: real) =
  for i in 0..length(v) do
    v[i] = v[i] * factor
  end for

// A planned `@strict` attribute will disable auto-clone insertion in
// this function body — every clone must then be written explicitly
// with .clone(). For performance-critical paths where every allocation
// must be visible. (Parser-reserved today; deferred.)

Higher-order functions

def apply_twice(f: real -> real, x: real) = f(f(x))

apply_twice(x -> x + 1.0, 5.0)        // 7.0
apply_twice(sqrt, 16.0)               // 2.0

Closures

def make_adder(a: real) =
  x -> x + a

val add5  = make_adder(5.0)
val add10 = make_adder(10.0)

add5(3.0)            // 8.0
add10(3.0)           // 13.0

Generic functions

A def may carry type parameters in [...] right after the name. The caller never writes the type arguments — the compiler infers each T from the actuals at the call site and emits a specialized clone per distinct instantiation.

def id[T](x: T): T = x

def pickMax[T: Ord](a: T, b: T): T =
  if a < b then b else a

id(42)               // T := integer
id("hi")             // T := string
pickMax(3, 7)        // 7   — T := integer
pickMax(2.5, 1.5)    // 2.5 — T := real

A bare [T] admits any type; a bound [T: Kind] restricts T to a fixed admission list — Numeric (integer / real / complex), Real, Float, Complex, Ord (the types with <), Eq (the types with ==). The closed set is hard-wired in this milestone; user-defined constraints are deferred.

Concrete overloads win when they apply — a generic is consulted only when no concrete def of the same name fits the call:

def f(x: integer): integer = 100
def f[T](x: T): integer    = 200

f(1)        // 100 — concrete integer overload
f("hi")     // 200 — falls through to the generic

Generic structs and enums use the same constraint set and specialization model — see the Generic structs and Generic enums sections below.

Generic structs

A struct declaration may carry type parameters in [...] after the name. Field types reference the parameters; the compiler emits a specialized clone per distinct argument combination.

struct Box[T]
  value: T
end

struct Pair[A, B]
  fst: A
  snd: B
end

val b = Box(42)                // T := integer
val p = Pair(1, "hi")          // A := integer, B := string
val q = Pair(1.0, 2.0)         // A := real, B := real

The constraint vocabulary is the same closed set as generic functions (Numeric, Real, Float, Complex, Ord, Eq, or bare Any). When call-site inference can’t pin every parameter, write an explicit type on the binding:

val pp: Pair[integer, string] = Pair(1, "hi")

A type parameter on a function may thread through to a generic struct constructor — the function’s specialization carries the right struct specialization along:

def pack[T](x: T, y: T): Pair[T, T] = Pair(x, y)

val a = pack(1, 2)             // Pair$integer$integer
val b = pack(1.5, 2.5)         // Pair$real$real

Generic enums

An enum may also take type parameters. Variant payloads name them; bare variants carry no payload but still belong to the specialization.

enum Opt[T] =
  Some(value: T)
  None

enum Result[T, E] =
  Ok(value: T)
  Err(error: E)

val x  = Some(42)              // Opt$integer
val n: Opt[integer] = None     // bare variant — annotation pins T
val r: Result[integer, string] = Ok(7)   // pins E that the use can't infer

A bare variant (None) carries no information to infer its enum’s type parameters from — supply a declared type on the binding to push the parameter down. Multi-parameter enums where a single use pins only some parameters likewise need an explicit type (“cannot infer type for type parameter E“ if you forget). Matching on a specialized enum uses ordinary patterns:

val msg = x match
  Some(v) -> "got " + str(v)
  None    -> "nothing"

Control flow

// `if` is an expression:
val abs_x = if x < 0.0 then -x else x

// `if` with no else has type `unit`:
if verbose then
  print("doing the thing")
end if

// `for` over ranges or arrays, with optional tuple destructuring:
for i in 0..n do
  process(i)
end for

for k, x in enumerate([10.0, 20.0, 30.0]) do
  print(s"index $k = $x")
end for

// `while` loop:
var n = 100
while n > 1 do
  if n % 2 == 0 then n = n // 2
  else                   n = 3*n + 1
end while

Constants

pi          // 3.14159265358979...
e           // 2.71828...
inf         // +infinity
nan         // a quiet NaN
i           // (0.0 + 1.0i) — imaginary unit

Prelude — math

sqrt(2.0)              // 1.414...
exp(1.0)               // e
log(e)                 // 1.0
log2(8.0); log10(100.0)
sin(pi); cos(0.0); tan(pi/4)
asin(1.0); atan2(1.0, 1.0)
sinh(1.0); cosh(1.0); tanh(1.0)
abs(-5)                // 5
sign(-3.0)             // -1.0
floor(2.7); ceil(2.1); round(2.5); trunc(2.9)
min(3, 5); max(3, 5)

Prelude — rank-1 array operations

val v = [1.0, 2.0, 3.0, 4.0, 5.0]

sum(v)                                          // 15.0
product(v)                                      // 120.0
min(v); max(v)                                  // 1.0; 5.0
dot(v, v)                                       // 55.0
length(v)                                       // 5

v.map(x -> x * x)                               // [1.0, 4.0, 9.0, 16.0, 25.0]
v.reduce(0.0, (acc, x) -> acc + x)              // 15.0  (left fold)
v.filter(x -> x > 2.0)                          // [3.0, 4.0, 5.0]

enumerate(v)                                    // [(0,1.0), (1,2.0), ...]
zip(v, [10.0, 20.0, 30.0, 40.0, 50.0])          // [(1.0,10.0), ...]
range(0, 5)                                     // [0, 1, 2, 3, 4]

Prelude — rank-2 (matrix) operations

val M = [[1.0, 2.0],
         [3.0, 4.0],
         [5.0, 6.0]]                            // 3×2

rows(M); cols(M); shape(M)                      // 3; 2; (3, 2)
transpose(M)                                    // 2×3
diag([1.0, 2.0])                                // [[1.0, 0.0], [0.0, 2.0]]  — diagonal matrix
reshape([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3)   // 2×3
flatten(M)                                      // [1.0, 3.0, 5.0, 2.0, 4.0, 6.0]  (column-major)

sum(M)                                          // 21.0  (all elements)
sum_axis(M, 0)                                  // [9.0, 12.0]  per-column
sum_axis(M, 1)                                  // [3.0, 7.0, 11.0]  per-row
M.map(x -> x + 1.0)                             // element-wise
matmul(transpose(M), M)                         // 2×2; same as transpose(M) @ M

Prelude — construction

zeros(5)                                        // [0, 0, 0, 0, 0]            — integer zeros
ones(3)                                         // [1, 1, 1]                  — integer ones
fill(4, 7.0)                                    // [7.0, 7.0, 7.0, 7.0]       — real, from x: real
fill(5, 0.0)                                    // [0.0, 0.0, 0.0, 0.0, 0.0]  — real zeros via fill
linspace(0.0, 1.0, 5)                           // [0.0, 0.25, 0.5, 0.75, 1.0]

zeros((2, 3))                                   // 2×3 integer zero matrix
ones((3, 3))                                    // 3×3 integer ones
fill((2, 2), 9)                                 // 2×2 of integer 9
fill((3, 2), 0.0)                               // 3×2 real zero matrix
identity(3)                                     // 3×3 integer identity matrix

Rank-2 construction takes a (rows, cols) tuple as the shape argument (single-integer arg = rank-1; tuple arg = rank-2). zeros/ones/identity currently return integer-element arrays — for real-typed initial buffers use fill(n, 0.0) / fill((r, c), 0.0) / linspace.

Prelude — I/O

print("hello")                                  // with newline
print()                                         // newline alone
print(s"x = $x")                                // plain interpolation
print(f"x = $x%5d, y = $y%.3f")                 // f-string with printf-style spec
val s = f"alpha = $a%8.4f"                      // value-position is fine too

f"..." accepts the same $ident / ${expr} interpolations as s"...", plus an optional Scala/printf-style spec right after each value: %[flags][width][.precision]conv with conv ∈ d f e g s x X o b. Flags - (left-align), 0 (zero-pad). See spec §10.6.

Prelude — conversions

to_real(42)                                     // 42.0
to_integer(3.7)                                 // 3 (truncates toward zero)
to_complex(5.0)                                 // (5.0 + 0.0i)

Testing

@test
def test_addition() =
  assert_eq(2 + 2, 4)

@test
def test_with_message() =
  assert(length([1.0, 2.0]) == 2, "expected length 2")

@test
def test_approx_real() =
  assert_approx(0.1 + 0.2, 0.3, 1e-10)

@test
def test_approx_complex() =
  // Checks `|a - b| <= tol` via hypot on the componentwise differences.
  assert_approx(1.0 + 2i, 1.000001 + 2.000001i, 1e-5)

@test
def test_approx_array() =
  // Element-wise rank-1 form: integer/real/complex elements are compared
  // per element with the same per-type distance as the scalar overloads.
  // A length mismatch traps.
  val xs = [1.0, 2.0, 3.0]
  val ys = [1.0 + 1e-12, 2.0 - 1e-12, 3.0 + 1e-12]
  assert_approx(xs, ys, 1e-9)

@test
def test_approx_array2() =
  // Same shape: rank-2 element-wise. Rows or cols mismatch traps.
  val A = [[1.0, 2.0], [3.0, 4.0]]
  val B = [[1.0 + 1e-12, 2.0 - 1e-12], [3.0, 4.0 + 1e-12]]
  assert_approx(A, B, 1e-9)

@test
def test_traps_on_bad_division() =
  assert_traps(() -> 1 div 0)

@test
def test_trap_message_contains_substring() =
  // The 2-arg form additionally checks that the trap's message
  // contains the expected substring — useful for asserting a specific
  // failure mode rather than "any trap fires".
  assert_traps(() -> 1 div 0, "division by zero")

A whole module can be marked test-only:

@test module mylib.deep_tests

import mylib.{public_fn}

// helper functions live alongside tests — both are stripped from production builds
def make_input(n: integer) = linspace(0.0, 1.0, n)

@test
def test_public_fn_monotonic() =
  val ys = make_input(100).map(public_fn)
  for i in 1..length(ys) do
    assert(ys[i] >= ys[i - 1])
  end for

Modules

Folder layout:

src/
  mylib/
    core.nexmodule mylib
    util.nexmodule mylib (same module, sibling file)
  main.nex
// in src/mylib/core.nex
module mylib

def public_fn(x: real) = x + 1.0

private def helper(x: real) = x * 2.0
// in src/mylib/util.nex
module mylib

// `helper` is visible because we're in the same module folder
def public_use_of_helper(x: real) = helper(x) + 1.0
// in src/main.nex
import mylib.{public_fn, public_use_of_helper}
import mylib.{public_fn as pf}      // rename on import

def main() =
  print(public_fn(3.0))             // 4.0
  print(pf(3.0))                    // 4.0
  // helper is NOT visible here — it's private to mylib

Literate Nex

A file with the .lnex extension is Markdown: column-0 prose, code indented one level (tab or four-plus spaces). The compiler strips the prose to blank lines (so error positions still match) and dedents the code before lexing. A module may freely mix .nex and .lnex files.

A short paragraph above the function explains why it exists.

    def square(x: real): real = x * x

A second paragraph between code blocks renders normally in
documentation and is invisible to the compiler.

    def main() =
      print(square(2.5))

See spec §2.9 for fenced-block handling and corner cases.

Search

Esc
to navigate to open Esc to close