Chapter 3

Syntax of Classes and Modules

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, union declarations, function declarations and definitions, type declarations, and statements.

program::=topitem*
topitem::=import | intern— module links
|classdec— class declaration
|uniondec— union declaration
|fundec | funsig— function definition / declaration
|tydec— type alias / refinement
|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⟩ ⟨deferredclass idgen
  ⟨note⟩ ⟨inherit
  classbody
  ⟨invariantend
inherit::=inherit parent (, parent)*
parent::=idtyargs
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.2.2Union Declarations

A union declaration introduces a closed set of data variants under a common type. It is a concise notation for a sealed hierarchy: the value of a union type is exactly one of its named variants, each of which may carry a list of named fields.

uniondec::=enumunion idgen⟩ ⟨notevariant+ end
variant::=id( paramlist )— tag and optional named payload

A union declaration is a derived form: it abbreviates declarations that could be written by hand. A declaration union P⟨gen⟩ with variants V1Vn elaborates to a sealed deferred class P together with one class Vi inherit P for each variant, whose fields are the variant’s payload and whose sole constructor make takes one parameter per field, in declaration order, and assigns it. The exact translation is given in Appendix C. Because the elaboration produces ordinary sealed classes, construction (create V.make(…)), generic arguments, matching, and the exhaustiveness guarantee of Section 4.4 all apply to a union with no further rules.

The union word is a soft keyword: it introduces a declaration only in top-level position, and remains usable as a member name (as in the union method of a set) elsewhere.

union Order
  Draft
  Placed(id: String, total: Real)
  Shipped(tracking: String, at: Date)
end

A union names data only: it synthesises no methods, invariants, or contracts on its variants. A variant that needs a constructor precondition, an invariant, or per-variant behaviour is written in the explicit sealed deferred class form of Section 3.2, which the union form does not replace.

When every variant is payload-free and P is non-generic, the declaration may be prefixed with the reserved word enum, making it an enumeration: a closed set of named, ordered, canonical values. The enum form still elaborates to the sealed hierarchy above, but the parent additionally becomes Comparable ordered by declaration order, each member is exposed as an interned class constant on the type (P.Vi, one canonical value per variant), and P.values is an array of all members in order; the full translation is in Appendix C. Because the enrichment occupies the names ordinal, compare, and values, no variant may bear them. A plain union is never so enriched; enum requests it.

enum union Color
  Red
  Green
  Blue
end

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::=privatefeature member+
member::=field | constant | method
field::=onceid : tynote
constant::=id: ty= expnote— class constant

A field declares an attribute of every instance. A field carries no initialiser: in a freshly created object a scalar field holds its zero value and an optional field holds nil, and every constructor must assign each non-optional reference field before it returns (Sections 4.9 and 5.5). 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).

A constant, written with = rather than the assignment symbol :=, does not declare an attribute: it names a value belonging to the class itself, fixed when the class is elaborated and immutable thereafter—an assignment to it is rejected statically. Within the class text a constant is referred to by its bare name, like a field; outside, it is accessed on the class, C.x, never on an instance. When the type annotation is omitted, the type is inferred from the initialising expression.

The initialising expression is unrestricted: as well as a scalar it may be an object (create …) or a collection display. Since the constant is fixed once when the class is elaborated, such a value is evaluated a single time and shared by every use, so an object- or collection-valued constant is one canonical value—C.x == C.x holds. An initialiser may name an earlier constant of the same class, or one inherited from a parent; a forward or cyclic reference among constants is rejected statically.

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⟩ ⟨alias⟩ ⟨note
  ⟨requiredo blockensure⟩ ⟨rescueend
|id (params): ty⟩ ⟨alias⟩ ⟨note⟩ ⟨deferred— deferred signature
alias::=alias opsym— binds an operator to this routine
opsym::="+" | "-" | "*" | "/" | "%" | "^"— a closed set
createsec::=create constructor+
constructor::=id(params)⟩ ⟨requiredo blockensure⟩ ⟨rescueend
fundec::=function idgen(params): ty⟩ ⟨note
  ⟨requiredo blockensure⟩ ⟨rescueend
funsig::=declare function idgen(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.

A one-argument routine may 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), and so the routine’s precondition and postcondition hold at the operator no less than at an explicit call. This is what makes the example above, money = old money - amount, meaningful for a class of one’s own and not only for the built-in numbers.

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

Three restrictions keep the notation closed. The set of aliasable operators is fixed—+ - * / % ^ and no others—so no program can introduce a symbol a reader has never met. Only arithmetic may be aliased: ordering is obtained by inheriting Comparable and defining compare, and value equality by defining equals (Section 5.3), not by aliasing. And an alias is consulted only where the operands are not already numeric (or, for +, a string), so no class can alter the meaning of + on Integer or Real. An alias is inherited: a routine aliased in a deferred class gives the operator to every heir, dispatching to the heir’s implementation.

The word alias is contextual, not reserved (Section 2.1): it has this meaning only in the position shown, and a program may still name a field, parameter, or routine alias. Adding the clause to the language therefore took no identifier away from any program that existed before it.

A deferred routine has no body The second form of method above—a signature followed by deferred and no doend—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 | Real
|Char | Boolean | String
|idtyargs— 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 = tyrefine— type alias or refinement
refine::=where id : exp— binder and predicate

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.5.1Refinement Types

When a declare type carries a where clause, it declares not an alias but a refinement type: the named base type narrowed by a predicate. The clause where n: e binds the value under test to n and gives a boolean expression e that every value of the refinement must satisfy.

declare type Quantity = Integer where n: n > 0
declare type Percentage = Real where p: p >= 0.0 and p <= 100.0

A refinement is not a class: it carries no fields, no constructor, and no boxing. A value of the refinement is a value of the base type—the refinement is a checked brand erased to the base representation, so a Quantity may be used wherever an Integer is wanted, and arithmetic on it yields the base type. The predicate is a contract: it is checked where a base value is narrowed into the refinement, and elided under skip-contracts like any other contract. The subtyping rule (narrowing checked, widening free) is given in Section 4.3, and the placement and evaluation of the check in Section 5.6. Like union, where is a soft keyword, recognised only after declare type id = ty.

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