Chapter 7

Programs

The preceding chapters define how each phrase of Nex is checked and evaluated. This short chapter puts them together into the meaning of a whole program: how its declarations and statements are processed, and in what order.

7.1The Two Worlds of a Program

As Section 3.1 describes, a program is a sequence of top-level items in any order, of two kinds. The declarations (the classes, free functions and type aliases, together with the module links that bring further declarations into scope) form the static world. The top-level statements form the dynamic world.

A program is processed by first building the static world in full, and then executing the dynamic world against it. This is not the order in which the items are written. Every declaration is in scope throughout every other, so classes and functions may refer to one another freely, and a top-level statement runs only after the whole static world exists. The programmer therefore need not arrange declarations in order of dependency, and a top-level statement may use a class or function declared later in the file.

print(twice(4))
let p := create Point.make()
print(p.x)

function twice(n: Integer): Integer do result := n * 2 end

class Point
  create make() do x := 9 end
feature
  x: Integer
end

The first three statements use a function and a class that are declared after them. The program prints 8 and then 9.

The two worlds are not wholly separate. A function or class body may read a variable declared by a top-level let, called a global, though it may not assign to one. Section 7.4 gives the rule that keeps such reads well defined.

7.2Building the Static World

The static world is built in three stages, starting from the standard environment of Appendix B: the classes and values that exist before any user code is loaded.

  1. Module links. Each import declaration adds a host class to the environment. Each intern declaration elaborates the named unit — recursively, by this same procedure — and adds all of its declarations, classes and free functions alike, not only the one named after the path.

    A class is added under the given name or alias, and also under its qualified name (Section 3.6.1). A function has no as and no qualified spelling of its own to be added under (Section 3.6.2), only its bare name. A unit is elaborated once however often it is interned, whether by the same path or by two different paths that resolve to it.

    A bare class name entered this way by more than one interned unit is an ambiguous reference (Section 4.2.1), rejected wherever it is written unqualified — except where the current unit declares a class of that name itself, which always takes precedence. A bare function name is subject to the same rule (Section 4.8.1), by a qualified call rather than a qualified name.

  2. Class and function registration. The names of all classes and all functions, including the signatures announced by declare, are put in scope (Section 4.2), so that every later step sees every name.
  3. Elaboration. Each class is elaborated: its parents are resolved, its fields and methods typed, and its contracts checked for well-formedness. Each function body is typed against the registered signatures. The class well-formedness conditions of Section 4.9 (acyclic inheritance, conformant overriding, complete deferral, sealed-implies-deferred) are verified here.

If any step fails, the program is rejected and nothing is executed.

A program that passes all three stages is statically valid. Its classes and functions, added to the standard environment, are the starting point for executing the top-level statements.

7.3Executing the Dynamic World

The top-level statements are then executed in the order they are written, by the rules of Chapter 5, as the statements of a single block. A variable declared by a top-level let is in scope for the statements after it, and each statement sees the objects as the statements before it left them.

The program’s observable behaviour is the combined effect of executing these statements: its output, the effects of the host classes it imports, and the objects it leaves behind.

If a top-level statement raises an exception that nothing handles, the program ends: the exception is reported, and no further statement runs.

A program that spawns tasks (Chapter 6) completes when its top-level statements have been evaluated and the host’s policy for outstanding tasks has been applied. The Definition fixes the meaning of each communication, but — as in Section 6.4 — leaves the disposal of unawaited tasks to the realisation.

7.4Readable Globals

A top-level let binds a global. Within the static world — the body of any free function or class method — a global is readable: an otherwise-unbound name that matches a global denotes that global’s current value.

A global is not assignable from the static world; an assignment to a global inside a function or class body is a static error. Ordinary scoping still applies, so a parameter, field, or local of the same name shadows the global throughout the body in which it is bound.

Because a global belongs to the dynamic world, it holds no value until the let that binds it has executed (Section 7.3). To keep every static-world read well-defined, the Definition imposes a def-before-use watermark.

Let the entry point be the first top-level statement, in source order, that transfers control into the static world — one that calls a free function or creates an instance of a user-defined class. (A method call need not be considered: its receiver must already have been created, so a create always precedes it.) The rule is:

Every global read by any function or class body must be bound by a let that stands, at top level, strictly before the entry point.

A program that violates the watermark is rejected before it runs.

The rule is sound because nothing in the static world executes before the entry point, so a global bound before that point is guaranteed to hold its value at every static-world read.

It is deliberately conservative: it treats all globals read anywhere in the static world uniformly, rather than tracing which body a given execution can reach. The programmer’s obligation is therefore simple — initialize the globals a program’s functions and classes depend on before the program begins calling into them.

7.5The Definition and the Implementation

The procedure of this chapter, building the static world in full before running the top-level statements, is the one the reference implementation follows (Section 1.3). That correspondence is intentional: the Definition is meant to describe the same language the implementation implements.

Every implementation of Nex, the reference compiler and the tree-walking interpreter alike, is required to reproduce the behaviour defined here. An implementation that diverges is in error: the divergence is a defect to be repaired, not a second meaning to be admitted.