Language
Classes (OOP)
Zornux reimagines objects with two everyday words: a class
describes a kind of thing, and an item is one made from it.
No new, no this, no self.
Defining a class
Fields are declared with has; behavior is a function inside the class. Inside a function, refer to fields by their plain name.
class Dog
has name
has breed
function bark
show name + " says Woof!"
end
end
A field may declare a type and a rule: has name as text required, has age as number, has active as truth = true. Types are text, number, whole, truth, date, and datetime; required rejects a missing value. Untyped has name still works — a type is opt-in.
Creating items
Make an item with create … from, then set its fields and call its functions.
create my_dog from Dog
my_dog.name = "Barnaby"
my_dog.breed = "Golden Retriever"
my_dog.bark() # Barnaby says Woof!
Constructors — when created
Give a class a when created block to set up each new item
as it is made, and pass values with create … with. A parameter
that shares a field's name initializes that field automatically.
class User
has name = "?"
has email = "?"
when created with name, email
# nothing to write — same-named parameters initialize both fields
end
end
create alice from User with "Alice", "[email protected]"
A constructor may validate its input and call the new item's own functions, but
it can't give back a value. If it fails, the error is
recoverable with try … catch error. A subclass with no
constructor uses its parent's. To transform an incoming value, take a
differently-named parameter — e.g. when created with raw then
email = lowercase(raw).
Properties — get / set, validation, computed
A plain has field is a data slot: reading or writing it never runs
code. Overlay a get/set accessor block and it becomes a
property — reading or writing can run code, carry validation, or
compute a value. Plain fields are unchanged; the behavior applies only where an
accessor block is present, and get/set stay contextual
words — no new reserved keyword.
class Signup
has email as text
get
set
required
email
end
has age as number
get
set
minimum 0
end
end
Declarative validators reuse the record rules — required,
not empty, minimum/maximum,
minimum length/maximum length, range,
email, url, and matches. They check the
incoming value before the setter runs, so a failed write raises the
recoverable ValidationFailed (ZX2504) and
leaves the previous value unchanged — never a partial mutation,
and the message never echoes the attempted value.
An accessor may carry a body. Inside a set, the incoming value is the
contextual name value; a get takes no input and must
give back a value. A getter with a body and no setter is a
computed property — no backing storage, not an ORM column, read
without parentheses.
class Product
private has stored_name as text
has name as text
get
give back stored_name
end
set
require value is not equal to "" otherwise "Name is required."
stored_name = trim(value)
end
end
has price as number
get
set
minimum 0
end
# Computed — read as item.label , with no parentheses.
has label as text
get
give back name + ": " + text(price)
end
end
end
A get with no set is read-only (writing it raises ZX0957); a set with no get is write-only — a password that is set but never read back (reading raises ZX0958). An accessor may narrow visibility with private set, never widen it (ZX0963).
validate item runs every property's declarative rules over its current
value without mutating it, returning the same is_valid /
errors result a record gives. An optional class-level
validate … end block adds whole-object rules that span several
properties; it runs only under validate item, not on every assignment.
create p from Product
p.name = " Widget " # setter trims → "Widget"
p.price = 9
create checked = validate p
show text(checked.is_valid) # true
show p.label # Widget: 9 (computed, no parentheses)
The four pillars, reimagined
Inheritance — extends
A class can build on another and inherit its fields and functions.
class GuideDog extends Dog
private has owner
function assist
show name + " is guiding " + owner
end
end
A subclass overrides an inherited function simply by declaring one with the
same name — there is no override keyword. Because anyone holding
a Dog may in fact be holding a GuideDog, an override
may not take away what the inherited function offered: it must accept every
call the inherited one accepts (an added parameter needs a default —
ZX0967), stay at least as visible (ZX0968), and stay
the same kind of member — an item function never becomes a
static one, or the reverse (ZX0969). All three are
reported by zornux check, before anything runs.
class Dog
function describe with prefix
give back prefix + "dog"
end
end
class GuideDog extends Dog
function describe with prefix, suffix = "" # legal: describe("a ") still works
give back base.describe(prefix) + suffix
end
end
Polymorphism
Iterate over a mix of items and call the same function — each item runs its own version.
for each animal in shelter
animal.speak() # each item answers in its own way
end
Encapsulation — access modifiers
Members are public by default. Opt into encapsulation with
protected (visible to the class and its subclasses) or
private (the declaring class only). The same modifiers work
on fields and methods.
| Modifier | Reachable from… |
|---|---|
public (default) | anywhere, including item.member |
protected | the class and any class that extends it |
private | the declaring class's own functions only |
class Account
has owner # public
protected has balance = 0 # visible to subclasses
private has pin # this class only
function set_pin with code
pin = code # ok — its own method
end
end
class Savings extends Account
function deposit with amount
balance = balance + amount # ok — protected reaches subclasses
give back balance
end
end
Reading item.member is always outside access, so it reaches public members only — encapsulated state is used by bare name inside the class hierarchy. A blocked access raises a clear, located diagnostic (ZX0904–ZX0920) rather than silently corrupting state.
Abstraction
Hide internal complexity behind a simple function, so callers work with intent, not machinery.
my_car.start() # the caller never sees the ignition sequence
Contracts — follows
A contract lists the functions (and fields) a class must
provide. A class promises to satisfy it with follows,
checked the moment it is declared — so a missing member is caught early,
not at the call site.
contract Animal
requires function speak
end
class Dog follows Animal
function speak
show "Woof"
end
end
class Cat follows Animal
function speak
show "Meow"
end
end
function make_speak with animal
animal.speak() # any Animal answers in its own way
end
A class may follow several contracts. requires function greet with who
pins the call the contract promises — an implementation may add a parameter with a
default, because that call still works. requires has name asks for
something a caller can read: a plain field satisfies it, and so does a
property with a get, including a computed one. Only a write-only
property fails (ZX0965), because nothing can read it. Contract-based
dispatch is ordinary polymorphism.
Contracts can follow contracts
A contract may build on others, and then requires everything they require, all the way up. A class that follows the outermost one must satisfy the whole chain — and is an instance of every contract along it. Two paths that require the same function are one requirement, not a conflict.
contract Printable
requires function print
end
contract Exportable follows Printable
requires function export_to with format
end
class Invoice follows Exportable
function print
show "invoice"
end
function export_to with format
give back format + ":invoice"
end
end
create i from Invoice
show text(i is an instance of Printable) # true — through Exportable
Across modules
Classes and contracts are checked across the whole project, so a contract in one module and the classes that follow it in another behave exactly as they would in one file. A type may be named through an import alias wherever a type is named — when creating an item, as a parent, as a contract, and in a runtime type check.
module Billing
import Models as m
public class Receipt extends m.Document follows m.Printable
public function print
show "receipt"
end
end
Relationship parameters — of T
What other languages call generics, Zornux writes as a
relationship. A function can name the relationship between what it takes and
what it gives back. of T introduces the name; follows — the same
word you already know from contracts — says what a T is expected
to behave like:
function clone of T follows Animal with value as T gives T
give back value
end
function combine of T follows Animal and Serializable, U with first as T, second as U gives T
give back first
end
Read it as a sentence: values related to T are expected to
behave as an Animal. Constraints are joined with and,
because inside an of clause the comma already separates one
relationship parameter from the next.
Callers never name Animal, runtime identity is unchanged, and an unknown value stays legal — nothing is rejected. This is documentation the toolchain can read, for editors and readers; it does not decide what the program does.
The toolchain already infers relationships and shows them in word style — List of whole, Map of text to whole, List of Point — flowing element types through map, filter, and indexing, and inferring a lambda's result per call. Hover a value in an LSP editor to see it. You write of T only when you want to state the intent yourself.
Abstract classes
Mark a class abstract when it exists only to be built on: it
can hold shared fields and functions but can't be made into an item directly.
Create items from a class that extends it.
abstract class Shape
function area
give back 0
end
end
class Square extends Shape
has side = 0
when created with side
end
function area
give back side * side
end
end
create sq from Square with 4
show text(sq.area()) # 16
Records and enums
A record is a lightweight value type: it compares
by value (two records with the same contents are equal),
validates with the same rules as a property, and — unlike a class — does not
map to the ORM. An enum names a fixed set of members, reached
as Enum.member; both stay contextual, so no new reserved word.
record Point
has x
has y
end
enum Suit
hearts
spades
clubs
diamonds
end
create origin from Point
origin.x = 0
origin.y = 0
show text(Suit.hearts) # hearts
Records, numbers, text, lists, and enum members compare by contents; class items compare by identity (unless the class overrides equals). A duplicate enum member is ZX0947, and an enum can't be constructed (ZX0948).
Text and equality, your way
By default an item prints as <Class item> — it never
exposes its fields — and is compares by identity. A class
can override either with a well-named function.
class User
has id = 0
has name = "?"
when created with id, name
id = id
name = name
end
function as_text
give back name # used by text(user) and show
end
function equals with other
give back id is other.id # used by user is other
end
end
create a from User with 1, "Alice"
create b from User with 1, "Alicia"
show text(a) # Alice
show text(a is b) # true — same id
An item is never turned into JSON automatically, and its default text hides every field, so private state can't leak through logs, output, or the debugger. as_text is the one place you choose what to reveal.
as_text renders an item — not an item inside a container.
show user uses it; show [user] prints
[<User item>]. That is deliberate: formatting a list, map, or set
is a plain walk over values and stays one, so showing, logging, or inspecting a
collection can never run your code — and therefore can never mutate state, perform
I/O, or fail. When you want each element rendered its own way, ask for it.
show join(map(users, function with one
give back text(one)
end), ", ")
Next: expose behavior to the world with Controllers.