Every construct in Nex — one card at a time.
-- This is a comment. The computer ignores it.
-- Use comments to explain your code to other humans!
let name: String := "Alice" -- a piece of text (String)
let age: Integer := 10 -- a whole number (Integer)
let height: Real := 4.5 -- a decimal number (Real)
let likes_cats: Boolean := true -- true or false (Boolean)
Real literals must include at least one digit after the decimal point. Valid examples: 4.5, 10.0, .5, 12.0e-3. Invalid examples: 10., 12.e-3.
print("Hello, world!")
print(name)
print("I am " + age + " years old")
let sum: Integer := 3 + 4 -- 7
let diff: Integer := 10 - 3 -- 7
let product: Integer := 5 * 6 -- 30
let quotient: Integer := 20 / 4 -- 5
let remainder: Integer := 10 % 3 -- 1
let power: Integer := 2 ^ 8 -- 256
Integer literals can also be written with explicit bases:
let flags: Integer := 0b1111_0000
let perms: Integer := 0o755
let color: Integer := 0xFF_AA_33
Use lowercase prefixes 0b, 0o, and 0x. _ may be used as a digit separator.
Integer is a 64-bit integer; Integer64 is another spelling of the same type.
Bytes and fixed-width integers. Byte, Integer16 and Integer32 are integers of an exact width, for binary data and protocols. Each is its own type, written with a suffix on an integer literal:
let b: Byte := 200u8 -- unsigned, 0..255
let port: Integer16 := 8080i16 -- signed, -32768..32767
let id: Integer32 := -70000i32 -- signed, -2147483648..2147483647
let mask: Byte := 0xFFu8 -- bases and `_` work as usual
An unsuffixed literal is always an Integer, so let b: Byte := 200 is a type error. A leading - is part of a signed literal: -5i16 is an Integer16.
None of Integer, Integer16, Integer32 and Byte converts to another on its own. Convert with a method; narrowing raises if the value does not fit:
let n: Integer := 300
let s: Integer16 := n.to_integer16() -- ok
let bb: Byte := n.to_byte() -- raises: 300 is not in 0..255
let back: Integer := s.to_integer()
Arithmetic on any of them gives an Integer, so it cannot overflow the narrow type; narrow the result again if you need to: (s + s).to_integer16(). Comparison and = are between two values of the same type.
Byte is also the element type of "text".to_bytes(), and create String.from_bytes(bytes) turns an Array[Byte] back into a String. Their methods are in the scalar types reference.
x = y -- equal?
x /= y -- not equal?
x == y -- same object?
x != y -- different object?
x < y x <= y -- less than? less or equal?
x > y x >= y -- greater than? greater or equal?
x and y -- both true?
x or y -- at least one true?
not x -- flip true/false
Nex has two kinds of equality:
= and /= compare by value== and != compare by identityValue equality checks whether two values have the same contents:
print([1, 2] = [1, 2]) -- true
print([1, 2] /= [1, 2]) -- false
print("abc" = "abc") -- true
For objects, =//= use the class's equals method. By default that is a structural, field-by-field comparison, but a class may override equals (and the matching hash) to define its own value equality. ==/!= ignore any override and always compare object identity.
Identity equality checks whether two variables refer to the same runtime object:
let a := [1, 2]
let b := a
let c := [1, 2]
print(a == b) -- true
print(a == c) -- false
print(a != c) -- true
For scalar values like numbers, booleans, characters, strings, and nil, == and != compare the values directly:
print(1 == 1) -- true
print("x" != "y") -- true
if age >= 18 then
print("You can vote!")
elseif age >= 13 then
print("You are a teenager")
else
print("You are a kid")
end
let label: String := when age >= 18 then "adult" else "minor" end
from let i: Integer := 1 until i > 5 do
print(i)
i := i + 1
end
-- prints 1 2 3 4 5
repeat 3 do
print("hello!")
end
-- prints hello! three times
Iterate over any collection (Array, String, Map) using its cursor:
across [10, 20, 30] as x do
print(x)
end
-- prints 10 20 30
Strings iterate by character:
across "abc" as ch do
print(ch)
end
-- prints #a #b #c
Maps iterate as Map_Entry[K, V] values, read-only key/value pairs with typed key and value members:
across {"name": "Alice", "age": "10"} as entry do
print(entry.key + ": " + entry.value)
end
-- prints "name: Alice" "age: 10"
An entry itself prints as "name": "Alice" (that is, print(entry)). For compatibility with earlier code, entry.get(0) and entry.get(1) still return the key and value, typed Any; prefer key and value.
function greet(name: String)
do
print("Hello, " + name + "!")
end
greet("Bob")
A function that gives back a value:
function double(n: Integer): Integer
do
result := n * 2
end
print(double(5)) -- 10
result is reserved for the value a routine returns. It cannot be declared as
anything else — a let, a parameter, a field, an across or match variable —
because the declaration would hide the return value and the routine would
silently return the default. Assign it with result := ...; choose another name
for everything else.
Every function in a program is checked against every other function's signature regardless of which is written first, so mutually recursive functions need no forward declaration at all:
function is_even(n: Integer): Boolean do
if n = 0 then
result := true
else
result := is_odd(n - 1)
end
end
function is_odd(n: Integer): Boolean do
if n = 0 then
result := false
else
result := is_even(n - 1)
end
end
declare function is available separately, to pin a function's signature
explicitly at a point before its real definition — useful for documentation,
or when the definition is far away — and the later definition is checked
against it exactly:
declare function is_even(n: Integer): Boolean
declare function is_odd(n: Integer): Boolean
function is_even(n: Integer): Boolean do
if n = 0 then result := true else result := is_odd(n - 1) end
end
function is_odd(n: Integer): Boolean do
if n = 0 then result := false else result := is_even(n - 1) end
end
Mutual recursion between let-bound closures works the same way: consecutive
closure-literal lets in one block are elaborated together, so any of them
may call any other, and one may call itself:
let is_even := fn(n: Integer): Boolean do
if n = 0 then result := true else result := is_odd(n - 1) end
end
let is_odd := fn(n: Integer): Boolean do
if n = 0 then result := false else result := is_even(n - 1) end
end
This works for a whole file (as bare top-level statements) and for a single,
multi-statement REPL input (wrap the statements in do ... end to submit
them to the REPL as one input, since the REPL otherwise treats each complete
statement as its own separate input) — including two or more closures that
merely share a captured, mutated variable without calling each other at
all (let total := 0 / let add := fn(x) do total := total + x end / let peek
:= fn(): Integer do result := total end). It does not currently work
across separate REPL inputs — defining both closures
in one input and then calling one from a later, separate input fails (a
limitation of the interactive session bridging interpreted and compiled
state, not of the language). A single self-recursive or self-mutating
closure (e.g. a counter) is unaffected either way; the gap is specific to
two or more closures that reference each other or share mutated state,
split across separate inputs. If you need that, use function (with
declare function if needed) instead of let — named functions are not
affected.
No default arguments. Nex has no default parameter values. A call must pass exactly as many arguments as the function (or method) declares.
Free functions cannot be overloaded either: each function name must be
unique, so you cannot define two greet functions that differ only in the
number of parameters. If you need variants, give them distinct names.
Class methods, however, can be overloaded by arity (see Optional arguments via method overloading under Classes), which is the idiomatic way to get optional-argument ergonomics in Nex.
let colors: Array [String] := ["red", "green", "blue"]
print(colors.get(0)) -- "red"
colors.add("yellow") -- add to the end
print(colors.length) -- 4
Bytes: Byte_Array. An Array[Byte] is a list of boxed values. For binary data, intern data/Byte_Array gives a fixed-size buffer stored as a real Java byte[]:
intern data/Byte_Array
let buf: Byte_Array := create Byte_Array.make(4)
buf.set(0, 200u8)
print(buf.get(0)) -- 200
print(buf.slice(0, 2)) -- Byte_Array([200, 0])
across buf as b do print(b) end -- b is a Byte
let pet: Map [String, String] := {"name": "Max", "kind": "dog"}
print(pet.get("name")) -- "Max"
pet.put("kind", "cat") -- update a value
Use a set when you want an unordered collection of distinct values.
let evens: Set[Integer] := #{0, 2, 4}
print(evens.contains(2)) -- true
An empty set uses the explicit set literal syntax:
let empty: Set[Integer] := #{}
You can also build a set from an array. Duplicate elements are removed:
let numbers: Set[Integer] := create Set[Integer].from_array([1, 2, 2, 3])
print(numbers.size) -- 3
Sets support the usual set operations:
let a: Set[Integer] := #{1, 2, 3}
let b: Set[Integer] := #{3, 4}
print(a.union(b)) -- #{1, 2, 3, 4}
print(a.intersection(b)) -- #{3}
print(a.difference(b)) -- #{1, 2}
Use spawn to start a lightweight task:
let t: Task[Integer] := spawn do
result := 1 + 2
end
print(t.await) -- 3
print(t.is_done) -- true (after completion)
If the spawn body does not assign result, the type is plain Task:
let t: Task := spawn do
print("background work")
end
Task operations:
await waits until the task finishesawait(ms) waits up to ms milliseconds and returns nil on timeoutcancel requests task cancellation and returns true if the task was cancelled before finishingis_done reports whether the task has finishedis_cancelled reports whether the task was cancelledawait_any([t1, t2, ...]) waits for the first task to finish and returns its resultawait_all([t1, t2, ...]) waits for all tasks and returns an array of resultsUse Channel[T] to communicate between tasks:
let ch: Channel[Integer] := create Channel[Integer]
spawn do
ch.send(42)
end
print(ch.receive) -- 42
ch.close
print(ch.is_closed) -- true
Use .with_capacity(n) for buffered channels:
let ch: Channel[Integer] := create Channel[Integer].with_capacity(2)
ch.send(10)
ch.send(20)
print(ch.size) -- 2
print(ch.capacity) -- 2
Channel operations:
send(value) blocks until acceptedsend(value, ms) waits up to ms milliseconds and returns true on success, false on timeoutreceive blocks until a value is availablereceive(ms) waits up to ms milliseconds and returns nil on timeouttry_send(value) returns true if the send succeeds immediately, otherwise falsetry_receive returns a value if one is immediately available, otherwise nilclose prevents future sends; buffered values may still be receivedis_closed, size, and capacity report channel stateUse select to wait on multiple channel operations or completed tasks:
select
when jobs.receive as job then
print(job)
when worker.await as value then
print(value)
when control.receive as signal then
print(signal)
timeout 1000 then
print("timed out")
else
print("idle")
end
select probes its clauses using try_send / try_receive for channels and is_done for tasks. Task clauses must use Task.await; they fire only when the task has already completed. If no clause is ready and there is no else, select waits until one becomes ready.
For full concurrency semantics and runtime details, see the Concurrency Guide.
A class bundles data and actions together:
class Pet
feature
name: String
sound: String
create
make(name: String, sound: String) do
this.name := name
this.sound := sound
end
feature
speak do
print(name + " says " + sound)
end
end
let cat: Pet := create Pet.make("Mimi", "meow")
cat.speak -- "Mimi says meow"
A constructor sets up an object when it is created:
class Circle
create
make(r: Real) do
radius := r
end
feature
radius: Real
area(): Real do
result := 3.14159 * (radius ^ 2)
end
end
let c: Circle := create Circle.make(5.0)
print(c.area) -- 78.53975
A field is set by assigning it (radius := r). Declaring a local with the
field's name (let radius := r) would only hide the field, so a let in a
class's routines may not reuse the name of a field or constant visible there,
including an inherited one. A parameter may, as in make(name: String) do
this.name := name end.
A constructor can delegate to another constructor of the same class with
this.<ctor-name>(...), so shared setup lives in one place:
class Box
feature
value: Integer
create
make(v: Integer) do
value := v
end
default do
this.make(0) -- delegates to make
end
end
let b: Box := create Box.default
print(b.value) -- 0
(To call a parent class's constructor instead, see Inheritance.)
Nex has no default parameter values, but a class may define several methods with the same name and different numbers of parameters. The right one is chosen by the number of arguments at the call site. Have the shorter version forward to the longer one to supply a default:
class Greeter
feature
greet(name: String): String do
result := greet(name, "!") -- forward with a chosen default
end
greet(name: String, punct: String): String do
result := "Hello, " + name + punct
end
end
let g: Greeter := create Greeter
print(g.greet("Ann")) -- "Hello, Ann!"
print(g.greet("Bob", ".")) -- "Hello, Bob."
Overloads are distinguished only by the number of arguments, not their types, so you cannot have two same-name methods with the same arity that differ only in parameter type. (Free functions cannot be overloaded at all — see Functions.)
A class can build on another class:
class Animal
feature
name: String
speak do print(name) end
create
with_name(n: String) do name := n end
end
class Dog
inherit Animal
feature
speak do
print(name + " says Woof!")
end
create
with_name(n: String) do Animal.with_name(n) end
end
Redeclaring queries as attributes. A heir may answer an inherited query (a routine with no arguments) with a stored attribute of the same name. Its type must conform to the query's return type. This is also how a deferred query is most often implemented:
deferred class Shape
feature
area(): Real deferred
describe: String do result := "area=" + area.to_string end
end
class Square
inherit Shape
feature
area: Real -- answers Shape's area
create
make(a: Real) do area := a end
end
let sh: Shape := create Square.make(4.0)
print(sh.describe) -- "area=4.0"
Clients and the parent's own code (describe above) read the attribute through
the query, and the query's inherited postconditions still apply to it. Inside
Square, area is the attribute, which only Square can assign. super.area
still reaches the parent's query.
The reverse is not allowed. An attribute cannot be redeclared as a routine: the class that declares the attribute, and its contracts, would keep using the stored value while every client saw the routine. So the parent's promises would describe something clients can no longer observe. To keep a value open to redefinition, store it under another name and publish it as a query:
class Account
feature
stored_balance: Integer
balance: Integer do result := stored_balance end
deposit(amount: Integer) do stored_balance := stored_balance + amount end
create
make(b: Integer) do stored_balance := b end
end
class Checking_Account
inherit Account
feature
required: Integer
balance: Integer do -- overrides the query
if super.balance > required then result := super.balance end
end
create
make(b, r: Integer) do super.make(b) required := r end
end
Clients write a.balance either way, whether the value is stored or computed.
A feature can bind itself to an arithmetic operator with an alias clause. The operator is then exactly sugar for the call: a - b is a.minus(b), so the feature's preconditions and postconditions apply at the operator too.
class Money
feature
once amount: Integer
once currency: String
minus(other: Money): Money
alias "-"
require
same_currency: currency = other.currency
do
result := create Money.make(amount - other.amount, currency)
end
create
make(a: Integer, c: String) do amount := a currency := c end
end
let owed: Money := create Money.make(100, "USD")
let paid: Money := create Money.make(30, "USD")
let rest: Money := owed - paid -- calls minus, checks same_currency
Three rules keep this from becoming the licence to invent notation that operator overloading usually is:
+, -, *, /, %, and ^ can be aliased. No program can introduce a symbol a reader has never seen.Comparable's compare, and = through equals; a class gets <, <=, >, >=, and = by inheriting Comparable and overriding those, not by aliasing.+), so no class can change what + means on Integer or Real.An alias is inherited: if a deferred parent declares plus … alias "+", then + works on any descendant, dispatching to its override.
alias is a soft keyword: it carries this meaning only in the position above, and stays available as an ordinary name anywhere else.
let alias := "vj" -- still a perfectly good variable
print(u.alias) -- and a perfectly good field
union is a keyword too, but narrower than alias: it still works as a member name (Set.union), but not as a general identifier — let union := 5 does not parse. where is not restricted at all; it is recognized only inside a refinement type's where n: <expr> clause (see Refinement Types) and is an ordinary identifier everywhere else, including let where := 5.
A class whose values are compared with = — including in a postcondition like ensure reduced: balance = old balance - amount — should also override equals (and hash), or = compares object identity rather than value.
A field declared with once can be set in a constructor but never reassigned afterward. The typechecker enforces this at compile time; the interpreter enforces it at runtime.
class Point
feature
once x: Integer
once y: Integer
create
make(px: Integer, py: Integer) do
x := px
y := py
end
end
let p: Point := create Point.make(3, 7)
-- p.x and p.y are now permanently 3 and 7
Attempting to assign a once field outside a constructor is a compile-time error:
class Box
feature
once value: Integer
create
make(v: Integer) do value := v end
feature
overwrite(v: Integer) do
value := v -- error: 'value' is a once field
end
end
A feature written NAME = expression (an initializer, no constructor) is a class-level constant, shared by every instance and read through the class name. The initializer is any expression — scalar, object, or collection — and is interned: evaluated once for the whole run, so every read returns the same value.
class Screen
feature
WIDTH = 1920
HEIGHT = 1080
AREA = WIDTH * HEIGHT -- may reference an earlier constant
end
print(Screen.WIDTH) -- 1920
print(Screen.AREA) -- 2073600
Because an object- or collection-valued constant is one shared value, C.x == C.x holds for it:
class Origin
feature
POINT = create Point.make(0, 0)
end
print(Origin.POINT.x) -- 0
print(Origin.POINT == Origin.POINT) -- true: the one canonical value
The initializer may reference an earlier constant of the same class or an inherited one; a forward or cyclic reference is a compile-time error.
Tell Nex what must be true before, after, and always:
class Wallet
feature
money: Real
spend(amount: Real)
require -- must be true BEFORE
enough: amount <= money
do
money := money - amount
ensure -- must be true AFTER
less: money = old money - amount
end
invariant -- must ALWAYS be true
not_negative: money >= 0.0
end
require, ensure, and invariant speak about a routine's boundaries or a class. assert states what must be true at one point inside a body:
assert enough_left: money >= 0.0 -- named: the failure reports the name
assert money >= 0.0 -- bare: the failure reports the line
assert -- several at once, like require
not_negative: money >= 0.0
under_limit: money <= 10000.0
A failed assert raises the same contract violation as a failed require or ensure. Assertions always run; there is no mode that strips them.
let attempts: Integer := 0
do
attempts := attempts + 1
if attempts < 3 then
raise "not ready yet"
end
print("done on attempt " + attempts)
rescue
print("failed, trying again...")
retry -- jump back to do and try again
end
raise throws an error. rescue catches it (the value is in exception). retry jumps back to the do block and runs it again from the top.
case direction of
"up" then print("going up")
"down" then print("going down")
else print("standing still")
end
A sealed modifier closes a class hierarchy. Only classes defined together with the sealed class can extend it. The typechecker knows the complete set of subclasses and can verify that every variant is handled.
A sealed class must be declared deferred — it cannot be instantiated directly, and the typechecker rejects a sealed class that is not also deferred. (Otherwise a bare instance of the parent would be a runtime value that an exhaustive match over its subclasses does not cover.)
sealed deferred class Result
end
class Ok
inherit Result
feature value: Integer
create make(v: Integer) do value := v end
end
class Err
inherit Result
feature msg: String
create make(m: String) do msg := m end
end
union)Writing a sealed hierarchy by hand is verbose: a sealed deferred class parent plus a full class … inherit … feature … create make for every variant. The union form is concise sugar for exactly that shape.
union Order
Draft
Placed(id: String, total: Real)
Shipped(tracking: String, at: Date)
end
This desugars to a sealed deferred class Order parent and one ordinary class per variant. Each variant's payload becomes feature fields plus an auto-generated make constructor, so construction and matching are the same as for hand-written sealed classes:
let o: Order := create Placed.make("A-100", 42.0)
match o of
Draft as d then print("draft")
Placed as p then print(p.id) -- payloads are ordinary fields
Shipped as s then print(s.tracking)
end
Draft) still gets a nullary make, so create Draft.make() works.union Result[T] gives Ok/Err that inherit Result[T].union is a sealed hierarchy after desugaring, match exhaustiveness is checked exactly as in the previous section — a missing variant is a compile-time error.The union form is deliberately data-only. When a variant needs its own contracts, invariants, or methods, write the explicit sealed deferred class form above; both compile to the same thing.
Enumerations (enum union). When every variant is payload-free, prefix the declaration with enum to get an enumeration — a closed set of named, ordered, canonical values:
enum union Color
Red
Green
Blue
end
On top of the plain union desugaring (a sealed hierarchy plus match exhaustiveness), enum adds three things:
print(Color.Red == Color.Red) -- true: interned, one canonical value
print(Color.Red < Color.Green) -- true: ordered by declaration order
print(Color.values.length()) -- 3: an Array[Color] of every member
across Color.values as c do print(c.ordinal) end -- 0 1 2
Color.Red is the one Red, so == (reference identity) holds — no allocation per use.Comparable: members compare by the order they are written.values is an Array[Color] of all members, in declaration order.enum is allowed only when every variant is payload-free and the union is non-generic — its members are canonical constants, which need a concrete type. A variant may not be named values, ordinal, or compare (those back the generated members). Plain union never gains any of this; the enrichment is opt-in.
let x: Integer := 10
do
let x: Integer := 99 -- shadows the outer x
print(x) -- 99
end
print(x) -- 10
convert <value> to <name>:<Type>
true if conversion succeeds, else false.<name> is bound to the converted value.<name> is bound to nil.<Type> must be a supertype or subtype of the runtime type of <value>.convert never changes a number's representation, so converting between Integer, Byte, Integer16, Integer32 and Real is rejected. Use to_byte(), to_integer16(), to_integer32(), to_integer() or to_real().do
convert vehicle_1 to my_car:Car
-- my_car is visible in this block
end
if convert vehicle_1 to my_car:Car then
my_car.sound_horn
end
?<expr> as <name>?<expr> as <name>
<expr> must have a detachable (?T) type. Returns true if <expr> is non-nil, else false.<name> is bound to <expr>'s attached (non-detachable) value for the branch this guards.x /= nil, <expr> can be any expression, not just a bare identifier — a dotted chain (?p.age as a) or a safe-navigation read (?p?.age as a) narrows directly, no intermediate let needed.and are each narrowed in turn, so a later conjunct can use an earlier one's binding:if ?p.age as a and ?q.age as b then
print(when a < b then "p is younger" else "p is not younger" end)
end
The standard library provides two sealed sum types for error handling and optional values, imported with intern:
intern data/Result
intern data/Option
let r: Result[Integer, String] := create Ok[Integer, String].make(10)
let doubled: Result[Integer, String] :=
result_map(r, fn (x: Integer): Integer do result := x * 2 end)
print(doubled.unwrap_or(0)) -- 20
let o: Option[Integer] := create Some[Integer].make(7)
print(o.get_or(0)) -- 7
Result[T, E] is Ok(value: T) or Err(error: E); Option[T] is Some(value: T) or None.is_ok(), is_err(), unwrap_or(fallback) on Result; is_some(), is_none(), get_or(fallback) on Option.result_map, result_and_then, result_map_err; option_map, option_and_then, option_filter. and_then is the bind that chains fallible steps and short-circuits on the first Err/None.create Ok.make(10) gives Ok[Integer, Any] — assignable to Result[Integer, String] (a parameter the constructor does not mention, like Err's value type here, stays Any and acts as a wildcard). Write explicit arguments (create Ok[Integer, String].make(…)) when you need to pin them.match dispatches on the runtime type of an expression. Used with a sealed parent class it becomes an exhaustive type switch — every variant must be handled or the typechecker rejects the program:
match r of
Ok as ok then
print(ok.value)
Err as err then
print(err.msg)
end
ClassName as var then clause binds var to the matched object, typed as ClassName. A clause's body is a single statement — use do ... end for more than one.else branch covers remaining cases and suppresses the exhaustiveness check:match r of
Ok as ok then
print(ok.value)
else
print("not ok")
end
Destructuring and wildcards. A clause may destructure the matched variant's payload fields by name, instead of
binding the whole object and reading .field:
match order of
Draft then print("draft")
Placed(id, total) then print(id) -- bind fields id, total
Shipped(tracking as t) then print(t) -- rename tracking→t, ignore at
end
Variant(a, b) binds the payload fields named a and b to locals of the same name; Variant(a as x) binds field a to a local x. Order does not matter — fields are matched by name, and a field you do not name is ignored.: means exactly one thing: this field has this type (total: Integer). Renaming is as. So Variant(a: x) does not bind x; it requires field a to be an x, and is an error unless x names a type. To compare a field to a value, use a guard.as still binds the whole value and composes with destructuring (Placed(id, total) as p then …). A clause may bind neither (Draft)._ is a catch-all, equivalent to else, and likewise suppresses the exhaustiveness check.Destructuring and _ are pure sugar over the type-dispatch form above, so
exhaustiveness and type checking behave identically (a destructured field the
variant does not have is a compile-time error).
Guards. A clause may add an if <boolean> guard, evaluated after the type match and
destructuring. A false guard falls through to the next clause:
match order of
Placed(id, total) if total > 1000 then flag(id)
Placed(id, total) then charge(id, total)
Draft then note_draft()
end
as binding.Boolean._, or else.Guards run on the JVM (compiled) and interpreter backends.
Comparing a field to a value. Use a guard. There is no literal field pattern: Move(dx: 0) was once sugar for
Move(dx) if dx == 0, and it is now rejected, with an error naming the guard to
write.
match cmd of
Move(dx, dy) if dx = 0 and dy = 0 then stay()
Move(dx, dy) then move(dx, dy)
Say(text) if text = "quit" then bye()
Say(text) then say(text)
end
The guard is not merely the surviving spelling — it is the better one. It binds
the field it constrains (the literal form did not, so a body naming that field
silently saw nil), and it leaves : with exactly one meaning in a field
pattern: this field has this type.
Type patterns. A field pattern may pin a field to a type with field: Type; the clause
matches only when the field is one at runtime, and binds the narrowed field
under its own name:
match shape of
Box(content: Circle) then print(content.radius) -- content is a Circle here
Box(content) then print("not a circle")
end
content: Any down to Circle); do not restate a known one.total: Integer), a user class, a parameterized type (items: Array[String]), or a declare type alias, which is tested as the type it names.declare type Quantity = Integer where n: n > 0) may not be used here, and is rejected. A type pattern is a runtime test, and a refinement's predicate is erased at runtime — the test could only check Integer and would match values Quantity excludes. Test the base type and put the predicate in a guard, or narrow with a typed let, which does run it.Nested patterns. A type pattern may go on to match the narrowed field's payload —
field: Type(sub-patterns):
match result of
Ok(value: Some[Integer](value as x)) then use(x) -- Ok whose value is a Some
_ then fallback()
end
inner: Some[Integer]) the narrowed field binds as inner.Some[Integer]) for the bound sub-fields to keep their element type; without them the sub-fields bind as Any.o.inner has its real type (Option[Integer], not Option[Any]) — which is what lets the nested test type-check.Nested patterns run on the JVM (compiled) and interpreter backends.
let add: Function(a: Integer, b: Integer): Integer :=
fn (a, b: Integer): Integer do result := a + b end
print(add(3, 4)) -- 7
Bare Function is still valid and compatible with any typed function value:
let f: Function := fn (n: Integer): Integer do result := n * 2 end
When the target already carries a concrete Function(...) type,
fn's own parameter and return types can be left out —
they're inferred from that target:
let add2: Function(a: Integer, b: Integer): Integer := fn(a, b) do result := a + b end
print(add2(3, 4)) -- 7
Inference only works where a target type is actually available (here, the
let's own annotation). With no such context, every parameter
needs its own explicit type, or it's a compile error.
A Function(...) type's parameter names are for documentation
only — they can be dropped, leaving just the types:
let is_positive: Function(Integer): Boolean := fn(n: Integer): Boolean do result := n > 0 end
declare type binds a name to any type expression — most commonly used to name a function signature for reuse:
declare type Transformer = Function(n: Integer): Integer
let double: Transformer := fn (n: Integer): Integer do result := n * 2 end
let square: Transformer := fn (n: Integer): Integer do result := n * n end
Any type can be aliased, not just function types:
declare type Matrix = Array[Array[Real]]
A declare type with a where clause is a refinement type: an existing type narrowed by a boolean predicate, without declaring a class.
declare type Quantity = Integer where n: n > 0
declare type Percentage = Real where p: p >= 0.0 and p <= 100.0
declare type NonEmpty = String where s: s.length() > 0
where n: <expr> binds the value under test to n (any name) and gives a boolean predicate over it, mirroring the label: condition shape of contracts.
A refinement is its base type with a checked constraint — not a class: no fields, no constructor, no wrapper. A Quantity is an Integer at runtime, so it interoperates freely with its base:
let q: Quantity := 5 -- checked: raises if the value is not > 0
let total: Integer := q + 10 -- free: a Quantity is an Integer
The predicate is enforced wherever a value is narrowed into the refinement — a let of that type, a parameter of that type (checked at the call boundary), and a return of that type. Widening (Quantity → Integer) is always free.
function debit(amount: Quantity): Integer do -- amount checked on entry
result := amount
end
Notes and current limits:
q + q is an Integer. Flow the result back into a refinement-typed binding to re-check it.convert targets, ?R detachable bindings, and distinct nominal newtypes are not yet checked/supported — use a class where you need those.class Box [T]
feature
value: T
create
make(v: T) do value := v end
end
let b: Box [Integer] := create Box[Integer].make(42)
b.value -- 42
Generic functions use the same bracket syntax after the function name:
function first[T](values: Array[T]): T do
result := values.get(0)
end
print(first([10, 20, 30])) -- 10
print(first(["a", "b", "c"])) -- "a"
Multiple generic parameters are allowed:
function pick_or_default[K, V](present: Boolean, value: V, fallback: V): V do
result := when present then value else fallback end
end
Anonymous functions can also declare generic parameters explicitly:
let id: Function := fn[T](x: T): T do
result := x
end
print(id(42)) -- 42
Constrained Generics. T -> Bound constrains a generic parameter to a type, and the body may then use the routines and fields of that bound on a value of type T. The bound may be a builtin such as Comparable, or any class you declare:
deferred class Shape
feature
area(): Integer deferred
name: String
end
function total_area[T -> Shape](xs: Array[T]): Integer do
result := 0
across xs as s do
result := result + s.area() -- a routine of the bound
end
end
Calls through a bound dispatch dynamically, so a deferred routine resolves to the runtime subclass's override. The bound itself must be a plain type name: a parameterized bound ([T -> Addable[T]]) is not yet supported.
x := 10 -- set an existing variable
let y: Integer := 20 -- create a new variable
this.name := "Nex" -- set a field inside a method
Nex reads like English: if...then...end, from...until...do...end, repeat...do...end, across...as...do...end, class...feature...end. Write what you mean, and Nex will check that you mean what you write.