A Nex program is more than a block of statements. It is a collection of classes—each bundling data, behaviour, and the contracts that govern them—together with free functions, module links, and the top-level statements that set the whole in motion. This chapter gives the grammar of that larger structure.
3.1Programs and Compilation Units
A program is a sequence of top-level items: import and intern declarations, class declarations, function declarations and definitions, type declarations, and statements.
| program | ::= | topitem* | |
| topitem | ::= | import | intern | — module links |
| | | classdec | — class declaration | |
| | | fundec | funsig | — function definition / declaration | |
| | | tydec | — type alias | |
| | | stmt | — top-level statement |
Although the items may be written in any order, they do not all take effect at once. The grammar’s order is not the order of execution: the declarations—classes, functions, and type aliases—constitute the static world of the program and are elaborated first, as a whole, so that they may refer to one another regardless of textual position; the top-level statements constitute the dynamic world and are executed afterwards, in source order, against the static world so established. This separation is made precise in Chapter 7.
3.2Class Declarations
A class declaration introduces a class: a named family of objects sharing a set of features and obeying a set of invariants.
| classdec | ::= | ⟨sealed⟩ ⟨deferred⟩ class id ⟨gen⟩ |
| ⟨note⟩ ⟨inherit⟩ | ||
| classbody | ||
| ⟨invariant⟩ end | ||
| inherit | ::= | inherit parent (, parent)* |
| parent | ::= | id ⟨tyargs⟩ |
| classbody | ::= | (featuresec | createsec)* |
| invariant | ::= | invariant assertion+ |
| note | ::= | note string |
The two modifiers control instantiation and extension. A
deferred class may not be instantiated; it serves as an interface
or partial implementation, to be completed by its heirs. A sealed
class closes its hierarchy: only classes declared in the same program may
inherit from it, and so the complete set of its descendants is known statically.
A sealed class must also be deferred (Section 4.9), which is why the two
modifiers so often appear together.
3.2.1Generic Parameters
A class or routine may be parameterised by one or more type variables, given
in square brackets after the name. A parameter may carry a single constraint,
written with ->, naming a class that any actual type argument must
conform to; and it may be marked with a leading ? to admit
nil as an argument.
| gen | ::= | [ genparam (, genparam)* ] | |
| genparam | ::= | ⟨?⟩ id ⟨-> id⟩ | — name, optional constraint |
| tyargs | ::= | [ ty (, ty)* ] |
Generics are ordinary types, not a notational convenience layered over an untyped core; their elaboration is given in Section 4.7.
3.3Features
The body of a class is a sequence of feature sections and
creation sections. A feature section introduces fields and routines; it
may be marked private, in which case its members are accessible
only from within the class.
| featuresec | ::= | ⟨private⟩ feature member+ | |
| member | ::= | field | method | |
| field | ::= | ⟨once⟩ id : ty ⟨:= exp⟩ ⟨note⟩ | |
| | | id := exp ⟨note⟩ | — field with inferred type |
A field declares an attribute of every instance. Its initialiser, if present,
gives the value the field holds in a freshly created object before any
constructor runs. A field marked once may be assigned within a
constructor but never afterwards; an attempt to assign it elsewhere is rejected
statically (Section 4.4).
3.4Routines and Contracts
A routine is a method, a constructor, or a free function. All three share one anatomy: an optional parameter list, an optional return type, an optional precondition, a body, an optional postcondition, and an optional rescue clause.
| method | ::= | id ⟨( ⟨params⟩ )⟩ ⟨: ty⟩ ⟨note⟩ | |
| ⟨require⟩ do block ⟨ensure⟩ ⟨rescue⟩ end | |||
| | | id ( ⟨params⟩ ) ⟨: ty⟩ ⟨note⟩ ⟨deferred⟩ | — deferred signature | |
| createsec | ::= | create constructor+ | |
| constructor | ::= | id ⟨( ⟨params⟩ )⟩ ⟨require⟩ do block ⟨ensure⟩ ⟨rescue⟩ end | |
| fundec | ::= | function id ⟨gen⟩ ( ⟨params⟩ ) ⟨: ty⟩ ⟨note⟩ | |
| ⟨require⟩ do block ⟨ensure⟩ ⟨rescue⟩ end | |||
| funsig | ::= | declare function id ⟨gen⟩ ( ⟨params⟩ ) ⟨: ty⟩ ⟨note⟩ | |
| params | ::= | param (, param)* | |
| param | ::= | id (, id)* ⟨: ty⟩ | — several names may share one type |
| require | ::= | require assertion+ | |
| ensure | ::= | ensure assertion+ | |
| rescue | ::= | rescue block | |
| assertion | ::= | id : exp | — a named boolean condition |
An assertion is a named boolean expression. The name has no
effect on meaning; it is the label by which a violation is reported. A
require clause states a precondition—an obligation on
the caller, checked on entry. An ensure clause states a
postcondition—a guarantee to the caller, checked on exit. A class
invariant states a condition every instance must satisfy whenever it
is observable from outside (Section 5.6). Together these are Nex’s
realisation of Design by Contract.
Within a postcondition, the form old e denotes the value that the
field e held when the routine was entered, allowing a guarantee to
relate the final state to the initial one, as in
money = old money - amount. A routine that declares a return type
delivers its result through the cell result, whose value when the
body finishes is the value of the call.
method above—a signature followed by
deferred and no do…end—declares
a routine whose implementation is supplied by heirs. It may appear only in a
deferred class. The declare function form plays the analogous role
for free functions: it announces a signature whose definition follows later,
which is how mutually recursive functions are written (Section 3.6).
3.5Type Expressions
A type expression denotes a type. The built-in scalar types and
Function are reserved names; a class name, possibly applied to type
arguments, denotes the corresponding class type; a leading ? forms
the optional type that additionally admits nil.
| ty | ::= | Integer | Integer64 | Real | Decimal | |
| | | Char | Boolean | String | ||
| | | id ⟨tyargs⟩ | — class type, possibly generic | |
| | | ? ty | — optional (nilable) type | |
| | | funty | — function type | |
| funty | ::= | Function ⟨( ⟨funtyparams⟩ ) ⟨: ty⟩⟩ | |
| funtyparams | ::= | funtyparam (, funtyparam)* | |
| funtyparam | ::= | id : ty | ty | — named or positional |
| tydec | ::= | declare type id = ty | — type alias |
The bare type Function, written without a signature, is the
unconstrained function type, compatible with any function value. A
declare type declaration binds a name to a type expression; the name
is thereafter interchangeable with that expression. Type aliases are most often
used to name a function signature, but any type may be aliased, as in
declare type Matrix = Array[Array[Real]].
3.6Modules
Nex keeps its core grammar small and pushes growth into libraries. Two declarations connect a program to code outside it.
An intern declaration loads another Nex source unit,
identified by a slash-separated path, optionally renaming it with
as. The named unit’s declarations become available to the
current program. An import declaration brings in a class from
the host platform—the Java virtual machine or the JavaScript
runtime—named by a dotted path and an optional source string.
| intern | ::= | intern id (/ id)* ⟨as id⟩ |
| import | ::= | import id (. id)* ⟨from string⟩ |
The intern mechanism is what allows the vocabulary of Nex to grow
without the grammar growing: new operations and conveniences live in library
units loaded by intern, not in new keywords. The meaning of these
declarations—which is, in essence, the elaboration of the named unit in the
current environment—is given in Chapter 7.
3.7Syntactic Restrictions
- A
sealedclass must also bedeferred(Section 4.9 explains why this is required rather than merely advised). - A field declared
oncemust give an explicit type; the inferred-type form offieldmay not be markedonce. - A deferred routine signature, and the
declare functionform, may not carryrequire,ensure, or a body. - A class may not inherit from itself, directly or through a cycle of parents; the inheritance relation must be a partial order (Section 4.5).
- The body of a constructor named in a
createsection may assign theoncefields of its class; no other routine may. - A later
functiondefinition must match its earlierdeclare functionsignature exactly in name, generic parameters, parameter types, and return type. - Free function names are intended to be unique: a free function, unlike a method, may not be overloaded by arity. (The current implementation does not enforce this; a repeated name silently supersedes the earlier definition. See the note on enforcement gaps in Section 4.9.)