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.nex ← module mylib
util.nex ← module 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.