Chapter 5. Statements, blocks, and control structures

Function, value, and class bodies contain procedural code that is executed when the function is invoked, the value evaluated, or the class instantiated. The code contains expressions and control directives and is organized using blocks and control structures.

Note: the Ceylon language has a recursive block structure—statements and declarations that are syntactically valid in the body of a toplevel declaration are, in general, also syntactically valid in the body of a nested declaration or of a control structure, and vice-versa.

5.1. Block structure and references

A body is a block, defined in §5.2 Blocks and statements, class body, defined in §4.5 Classes, interface body, defined in §4.4 Interfaces, or comprehension clause, defined in §6.6.6 Comprehensions. Every body (except for a comprehension clause) is list of semicolon-delimited statements, control structures, and declarations, surrounded by braces. Some bodies end in a control directive. Every program element in the list is said to directly occur in the body. A program element directly occurs earlier than a second program element if both program elements directly occur in a body and the first program element occurs (lexically) earlier in the list than the second program element.

A program element (indirectly) occurs in a body if:

  • the program element directly occurs in the body, or

  • the program element indirectly occurs inside the body of a declaration or control structure that occurs directly in the body.

We sometimes say that the body contains the program element if the program element (indirectly) occurs in the body.

A program element (indirectly) occurs earlier than a second program element if:

  • the two program elements both directly occur in the same body, and the second program element occurs after the first program element, or

  • the second program element indirectly occurs inside the body of a declaration or control structure, and the first program element directly occurs earlier than the declaration or control structure.

Then we also say that the second program element (indirectly) occurs later than the first. The set of program elements that occur later than a program element is sometimes called the lexical scope of the program element.

A program element sequentially occurs in a body if:

  • the program element directly occurs in the body, or

  • the program element sequentially occurs inside the body of a control structure that occurs directly in the body.

A program element sequentially occurs earlier than a second program element if:

  • the two program elements both directly occur in the same body, and the second program element occurs after the first program element, or

  • the second program element sequentially occurs inside the body of a control structure, and the first program element directly occurs earlier than the declaration or control structure.

If a program element sequentially occurs earlier than a second program element, the sequence of statements from the first program element to the second program element comprises:

  • the sequence of statements that occur directly in the body in which the first program element directly occurs, beginning from the first program element and ending with the second program element, if the second program element occurs directly in the same body as the first program element, or

  • the sequence of statements that occur directly in the body in which the first program element directly occurs, beginning from the first program element and ending with the declaration or control structure in whose body the second program element sequentially occurs, followed by the sequence of statements from the first statement of the declaration whose body contains the second program element to the second program element itself, otherwise.

5.1.1. Declaration name uniqueness

A program element is contained within the namespace of a declaration if either:

  • the declaration is a toplevel declaration, and the program element is a toplevel declaration of the same package,

  • the declaration directly occurs in a body, and the program element sequentially occurs in the same body,

  • the declaration is a parameter or type parameter, and the program element sequentially occurs in the body of the parameterized declaration, or

  • the program element is a control structure variable or iteration variable of a control structure that sequentially occurs in the namespace of the declaration.

The namespace of a declaration may not contain a second declaration with the same name. For example, the following is illegal:

function fun(Float number) {
    if (number<0.0) {
        Float number = 1.0; //error
        ...
    }
    ...
}

A class or interface may not inherit a declaration with the same name as a declaration it contains unless either:

  • the contained declaration directly or indirectly refines the inherited declaration,

  • the contained declaration is not shared, or

  • the inherited declaration is not shared.

A class or interface may not inherit two declarations with the same name unless either:

  • the class or interface contains a declaration that directly or indirectly refines both the inherited declarations (in which case both the inherited declarations directly or indirectly refine some member of a common supertype, as required by §4.5.6 Member class refinement, §4.8.7 Attribute refinement, and §4.7.8 Method refinement),

  • one of the inherited declarations directly or indirectly refines the other inherited declaration, or

  • at least one of the inherited declarations is not shared.

5.1.2. Scope of a declaration

The scope of a declaration is governed by the body or package in which it occurs. A declaration is in scope at a program element if and only if either:

  • the declaration is a parameter or type parameter of a declaration whose body contains the program element,

  • the declaration is a control structure variable or iteration variable belonging to a block of a control structure that contains the program element,

  • the program element belongs to or is contained in the body of the declaration itself,

  • the program element belongs to or is contained in the body of a class or interface which inherits the declaration,

  • the declaration directly occurs in a body containing the program element,

  • the declaration is imported by the compilation unit containing the program element and is visible to the program element, or

  • the declaration is a toplevel declaration in the package containing the program element.

Where:

  • A control structure variable or iteration variable belongs to a block of a control structure if the block immediately follows the declaration of the variable.

  • A program element belongs to a declaration if it occurs in the extends, satisfies, of, or given clause of the declaration.

Furthermore:

  • A condition variable of a condition belonging to a condition list is in scope in any condition of the same condition list that occurs lexically later.

  • A resource expression variable of a try statement is in scope in any resource expression of the same resource expression list that occurs lexically later.

  • An iteration variable or condition variable of a comprehension is in scope in any clause of the comprehension that occurs lexically later, since comprehension clauses are viewed as nested bodies.

And finally, there are special rules for annotation lists, defined in §7.1.1 Annotation lists:

  • An annotation argument list belongs to the annotated declaration.

  • An annotation name is considered to occur directly in the compilation unit containing the program element.

Note: if no reference to an un-shared declaration occurs within the scope of the declaration, a compiler warning is produced.

5.1.3. Visibility

Classes, interfaces, functions, values, aliases, and type parameters have names. Occurrence of a name in code implies a hard dependency from the code in which the name occurs to the schema of the named declaration. We say that a class, interface, value, function, alias, or type parameter is visible to a certain program element if its name may occur in the code that belongs to that program element.

The visibility of a declaration depends upon where it occurs, and upon whether it is annotated shared. A toplevel or member declaration may be annotated shared:

  • If a toplevel declaration is annotated shared, it is visible wherever the package that contains it is visible. Otherwise, a toplevel declaration is visible only to code in the package containing its compilation unit.

  • If a member declaration is annotated shared, it is visible wherever the class or interface that contains it is visible. Otherwise, a declaration that occurs directly inside a class or interface body is visible only inside the class or interface declaration.

A type parameter or a declaration that occurs directly inside a block (the body of a function, getter, setter, or control structure) may not be annotated shared.

  • A type parameter is visible only inside the declaration to which it belongs.

  • A declaration that occurs directly inside a block is visible only inside the block.

TODO: Should we allow you to limit the effect of the shared annotation by specifying a containing program element or package?

We say that a type is visible to a certain program element if it is formed from references to classes, interfaces, type parameters, and type aliases whose declarations are visible to the program element. For shared declarations:

  • The type of a value must be visible everywhere the value itself is visible.

  • The return type of a function must be visible everywhere the function itself is visible.

  • The satisfied interfaces of a class or interface must be visible everywhere the class or interface itself is visible.

  • The superclass of a class must be visible everywhere the class itself is visible.

  • The aliased type of a class alias, interface alias, or type alias must be visible everywhere the alias itself is visible.

5.1.4. Hidden declarations

If two declarations with the same name or imported name, as defined in §4.2.5 Imported name, are both in scope at a certain program element, then one declaration may hide the other declaration.

  • If an inner body is contained (directly or indirectly) in an outer body, a declaration that is in scope in the inner body but is not in scope in the outer body hides a declaration that is in scope in the outer body. (In particular, a declaration inherited by a nested class or interface hides a declaration of the containing body.)

  • An un-shared declaration occurring directly in the body of a class containing the program element hides a declaration inherited by the class.

  • An actual declaration hides the declaration it refines.

  • A declaration occurring in a body containing the program element hides a declaration imported by the compilation unit containing the body or implicitly imported from the module ceylon.language.

  • A toplevel declaration of the package containing the program element hides a declaration implicitly imported from the module ceylon.language.

  • A declaration explicitly imported by the compilation unit containing the program element hides a declaration implicitly imported from the module ceylon.language.

  • A declaration explicitly imported by the compilation unit containing the program element hides a toplevel declaration of the package containing the compilation unit.

  • A declaration explicity imported by name in the compilation unit containing the program element hides a declaration explicitly imported by wildcard in the compilation unit.

For example, the following code is legal:

class Person(name) {
    String name;
    shared String lowerCaseName {
        String name = this.name.lowercased;
        return name;
    }
}

As is this code:

class Point(x, y) {
    shared Float x; 
    shared Float y;
}

class Complex(Float x, Float y=0.0) 
        extends Point(x, y) {}

When a member of a class is hidden by a nested declaration, the member may be accessed via the self reference this, defined in §6.3.1 this, or via the outer instance reference outer, defined in §6.3.2 outer.

shared class Item(name) {
    variable String name;
    shared void changeName(String name) {
        this.name = name;
    }
}
class Catalog(name) {
    shared String name;
    class Schema(name) {
        shared String name;
        Catalog catalog => outer;
        String catalogName => outer.name;
        class Table(name) {
            shared String name;
            Schema schema => outer;
            String schemaName => outer.name;
            String catalogName => catalog.name;
        }
    }
}

When a toplevel declaration of a package is hidden by another declaration, the toplevel declaration may be accessed via the containing package reference package, defined in §6.3.4 package.

Integer n => 0;
Integer f(Integer n) => n+package.n;

5.1.5. References and block structure

A declaration may be in scope at a program element, but not referenceable at the program element. A declaration is referenceable at a program element if the declaration is in scope at the program element and either:

  • the program element occurs within the lexical scope of the declaration, or

  • the declaration does not directly occur in a block or in the initializer section of a class body.

Note that these rules have very different consequences for:

Declarations that occurs in a block or class initializer section are interspersed with procedural code that initializes references. Therefore, a program element in a block or initializer may not refer to a declaration that occurs later in the block or class body. This restriction does not apply to declarations that occur in an interface body or class declaration section. Nor does it apply to toplevel declarations, which are not considered to have a well-defined order.

The following toplevel function declarations, belonging to the same package, are legal:

Float x => y;
Float y => x;

This code is not legal, since the body of a function is an ordinary block:

Float->Float xy() {
    Float x => y;  //compiler error: y is not referenceable
    Float y => x;
    return x->y;
}

This code is not legal, since all three statements occur in the initializer section of the class body:

class Point() {
    Float x => y;  //compiler error: y is not referenceable
    Float y => x;
    Float->Float xy = x->y;
}

However, this code is legal, since the statements occur in the declaration section of the class body:

class Point() {
    Float x => y;
    Float y => x;
}

Likewise, this code is legal, since the statements occur in an interface body:

interface Point {
    Float x => y;
    Float y => x;
}

5.1.6. Type inference and block structure

A value declared using the keyword value or a function declared using the keyword function may be in scope at a program element, but its type may not be inferrable, as defined by §3.2.9 Type inference, from the point of view of that program element.

The type of a value or function declared using the keyword value or function is inferrable to a program element if the declaration is in scope at the program element and the program element occurs within the lexical scope of the declaration.

Note: the type of a value or function declared using the keyword value or function is not inferrable within the body of the value or function itself.

For any other declaration, including any declaration which explicitly specifies its type, the type is considered inferrable to a program element if the declaration is in scope at the program element.

The following code is not legal:

interface Point {
    value x => y;  //compiler error: type of y is not inferrable
    value y => x;
}

However, this code is legal:

interface Point {
    value x => y;
    Float y => x;
}

5.1.7. Unqualified reference resolution

An unqualified reference is:

  • the type name in an unqualified type declaration or type argument, as defined by §3.2.7 Type expressions, for example String and Sequence in Sequence<String>,

  • the value, function, or type name in a base expression, as defined by §6.5.1 Base expressions, for example counter in counter.count, entries and people in entries(people*.name), or Entry, name, and item in Entry(name,item), or

  • the type name in an unqualified type in a static expression, as defined by §6.5.5 Static expressions, for example Sequence in Sequence.iterator.

If a program element contains an unqualified reference:

  • there must be at least one declaration with the given name or imported name, as defined in §4.2.5 Imported name, in scope at the program element, and

  • if multiple declarations with the given name or imported name are in scope at the program element where the given name occurs, then it is guaranteed by the type system and §5.1.1 Declaration name uniqueness that there is exactly one such declaration which is not hidden by any other declaration.

Then the reference is to this unique unhidden declaration, and:

  • the declaration must be referenceable at the program element,

  • the type of the declaration must be inferrable to the program element, and

  • if the declaration is forward-declared, it must be definitely initialized at the program element.

As a special exception to the above, if there is no declaration with the given name or imported name in scope at the program element and the program element occurs inside a dynamic block, then the unqualified reference does not refer to any statically typed declaration.

If an unqualified reference refers to a member declaration of a type, then there is a unique inheriting or declaring class or interface for the unqualified reference, that is, the unique class or interface in whose body the unqualified reference occurs, and which declares or inherits the member declaration, and for which the member is not hidden at the program element where the unqualified reference occurs.

5.1.8. Qualified reference resolution

A qualified reference is:

  • the type name in a qualified type declaration or type argument, as defined by §3.2.7 Type expressions, for example Buffer in BufferedReader.Buffer,

  • the value, function, or type name in a member expression, as defined by §6.5.2 Member expressions, for example count in counter.count, split in text.split(), or Buffer in br.Buffer(),

  • the type name in a qualified type in a static expression, as defined by §6.5.5 Static expressions, for example Buffer in BufferedReader.Buffer.size, or the member name in a static expression, for example iterator in Sequence.iterator, or size in BufferedReader.Buffer.size.

Every qualified reference has a qualifying type:

  • For a type declaration, the qualifying type is the full qualified type the qualifies the type name.

  • For a value reference or callable reference, the qualifying type is the type of the receiver expression.

  • For a static reference, the qualifying type is the full qualified type the qualifies the type or member name.

A qualified reference may not have Nothing as the qualifying type.

If a program element contains a qualified reference:

  • the qualifying type must have or inherit at least one member or nested type with the given name or imported name, as defined in §4.2.5 Imported name, which is visible at the program element, and

  • if there are multiple visible members with the given name or imported name, then it is guaranteed by the type system and §5.1.1 Declaration name uniqueness that there is exactly one such member which is not refined by another member, except

  • if the qualifying type inherits a class or interface that contains the program element, and an un-shared declaration contained directly in the body of this class or interface has the same name as a shared member of the qualifying type, in which case the un-shared declaration hides the shared member, or

  • if the qualifying type is an intersection type, in which case there may be multiple members which are not refined by another member, but where there is exactly one such member that is refined by each of these members, but is not refined by another member that is refined by all of these members, except

  • in the case of certain pathological intersection types, where two of the intersected types declare distinct members with the same name, that do not refine any member of a common supertype (in which case what we actually have are disjoint types that are nevertheless not considered provably disjoint within the rules of the typesystem), and in this case the qualified reference is considered illegal.

Then the reference is to the unique member or nested class. If the program element is contained in the body of a class or interface, and the member declaration directly occurs in the body of the class or interface, and the qualified reference is a value reference or callable reference, and the receiver expression is a self reference to the instance being initialized, then:

  • the member declaration must be referenceable at the program element,

  • the type of the member must be inferrable to the program element, and

  • if the member declaration is forward-declared, it must be definitely initialized at the program element.

As a special exception to the above, if the program element occurs inside a dynamic block, and the the receiver expression has no type, then the qualified reference does not refer to any statically typed declaration.

5.2. Blocks and statements

A block is list of semicolon-delimited statements, control structures, and declarations, surrounded by braces.

Block: "{" (Declaration | Statement)* "}"

A statement is an assignment or specification, an invocation of a method, an instantiation of a class, a control structure, a control directive, or an assertion.

Statement: ExpressionStatement | Specification | Assertion | DirectiveStatement | ControlStructure

A statement or declaration contained in a block may not evaluate a value, invoke a function, instantiate a class, or extend a class whose declaration occurs later in the block.

5.2.1. Expression statements

Only certain expressions are valid statements:

  • assignment,

  • prefix or postfix increment or decrement,

  • invocation of a method,

  • instantiation of a class.

ExpressionStatement: ( Assignment | IncrementOrDecrement | Invocation ) ";"

For example:

x += 1;
x++;
print("Hello");
Main(process.arguments);

5.2.2. Control directives

A control directive statement ends execution of the current block and forces the flow of execution to resume in some outer scope. They may only occur as the lexically last statement of a block.

DirectiveStatement: Directive ";"

There are four control directives:

  • the return directive—to return a value from a getter or non-void function or terminate execution of a setter, class initializer, or void method,
  • the break directive—to terminate a loop,
  • the continue directive—to jump to the next iteration of a loop, and
  • the throw directive—to raise an exception.
Directive: Return | Throw | Break | Continue

For example:

throw Exception();
return x+y;
break;
continue;

The return directive must sequentially occur in the body of a function, getter, setter, or class initializer. In the case of a setter, class initializer, or void function, no expression may be specified. In the case of a getter or non-void function, an expression must be specified. The expression type must be assignable to the return type of the function or the type of the value. When the directive is executed, the expression is evaluated to determine the return value of the function or getter.

Return: "return" Expression?

If the specified expression has no type, or if the function or getter has no type, and the directive occurs within a dynamic block, then the directive is not type-checked at compile time.

Note: a return statement returns only from the innermost function, getter, setter, or class initializer, even in the case of a nested or anonymous function. There are no "non-local returns" in the language.

The break directive must sequentially occur in the body of a loop.

Break: "break"

The continue directive must sequentially occur in the body of a loop.

Continue: "continue"

A throw directive may appear anywhere and may specify an expression, whose type must be a subtype of type Throwable defined in ceylon.language. When the directive is executed, the expression is evaluated and the resulting exception is thrown. If no expression is specified, the directive is equivalent to throw Exception().

Throw: "throw" Expression?

If the specified expression has no type, and the directive occurs within a dynamic block, then the directive is not type-checked at compile time.

5.2.3. Specification statements

A specification statement may specify or initialize the persistent value of a forward-declared reference, or specify the implementation of a forward-declared getter or function.

Specification: ValueSpecification | LazySpecification

The persistent value of a forward-declared reference or the implementation of a forward-declared function may be specified by a value specification statement. The value specification statement consists of an unqualified value reference and an ordinary = specifier. The value reference must refer to a declaration which sequentially occurs earlier in the body in which the specification statement occurs.

ValueSpecification: MemberName Specifier ";"

The type of the specified expression must be assignable to the type of the reference, or to the callable type of the function.

If the specified expression has no type, or if the reference or function has no type, and the specification occurs within a dynamic block, then the specification is not type-checked at compile time.

String greeting;
if (exists name) {
    greeting = "hello ``name``";
}
else {
    greeting = "hello world";
}
String process(String input);
if (normalize) {
    process = String.normalized;
}
else {
    process = (String s) => s;
}

Note: there is an apparent ambiguity here. Is the statement x=1; a value specification statement, or an assignment expression statement? The language resolves this ambiguity by favoring the interpretation as a specification statement whenever that interpretation is viable. This is a transparent solution, since it accepts strictly more code than the alternative interpretation, and for ambiguous cases the actual semantics are identical between the two interpretations.

The implementation of forward-declared getter or function may be specified using a lazy specification statement. The specification statement consists of either:

  • an unqualified value reference and a lazy => specifier, or

  • a unqualified callable reference, one or more parameter lists, and a lazy specifier.

The value reference or callable reference must refer to a declaration which sequentially occurs earlier in the body in which the specification statement occurs.

A callable reference followed by a parameter list is itself considered a callable reference, called a parameterized reference. If the parameter list has type P then the callable reference must have the exact type Callable<R,P> for some type R. Then the type of the parameterized reference is R.

ParameterizedReference: MemberName Parameters+

Thus, the specification statement consists of a parameterized reference followed by a lazy specifier.

LazySpecification: (MemberName | ParameterizedReference) LazySpecifier ";"

The type of the specified expression must be assignable to the type of the parameterized reference, or to the type of the value reference.

String greeting;
if (exists name) {
    greeting => "hello ``name``";
}
else {
    greeting => "hello world";
}
String process(String input);
if (normalize) {
    process(String input) => input.normalized;
}
else {
    process(String s) => s;
}

5.2.4. Definite return

A sequence of statements may definitely return.

  • A sequence of statements definitely returns if it ends in a return or throw directive, or in a control structure that definitely returns, or contains an assertion with a condition list that is never satisfied.

  • A body definitely returns if it contains a list of statements that definitely returns.

  • An if conditional definitely returns if it has an else block and both the if and else blocks definitely return, or if its condition list is always satisfied and the if block definitely returns, or if its condition list is never satisfied and it has an else block that definitely returns.

  • A switch conditional definitely returns if all case blocks definitely return and the else block, if any, definitely returns.

  • A for loop definitely returns if it has an else block that definitely returns, and there is no break directive in the for block, or if the iterated expression type is a nonempty type, and the for block definitely returns.

  • A while loop definitely returns if its condition list is always satisfied and the while block definitely returns.

  • A try/catch exception manager definitely returns if the try block definitely returns and all catch blocks definitely return or if the finally block definitely returns.

The body of a non-void method or getter must definitely return.

A body may not contain an additional statement, control structure, or declaration following a sequence of statements that definitely returns. Such a statement, control structure, or declaration is considered unreachable.

5.2.5. Definite initialization

A sequence of statements may definitely initialize a forward-declared declaration.

  • A sequence of statements definitely initializes a declaration if one of the statements is a specification statement or assigment expression for the declaration or a control structure that definitely initializes the declaration, or if the sequence of statements ends in a return or throw directive, or contains an assertion with a condition list that is never satisfied.

  • An if conditional definitely initializes a declaration if it has an else block and both the if and else blocks definitely initialize the declaration, of if its condition list is always satisfied and the if block definitely initializes the declaration, of if its condition list is never satisfied and it has an else block that definitely initializes the declaration.

  • A switch conditional definitely initializes a declaration if all case blocks definitely initialize the declaration and the else block, if any, definitely initializes the declaration.

  • A for loop definitely initializes a declaration if it has an else block that definitely initializes the declaration, and there is no break directive in the for block, or if the iterated expression type is a nonempty type, and the for block definitely initializes the declaration.

  • A while loop definitely initializes a declaration if its condition list is always satisfied and the while block definitely initializes the declaration.

  • A try/catch exception manager definitely initializes a declaration if the try block definitely initializes the declaration and all catch blocks definitely initialize the declaration or if the finally block definitely initializes the declaration.

TODO: an assignment expression occurring within a containing expression may or may not definitely initialize a value. Specify this!

If a function or value declaration is referenceable at a certain statement or declaration, it may additionally considered definitely initialized at that statement or declaration.

If a function declaration is definitely initialized at a certain statement or declaration if it is referenceable at that statement or declaration and:

  • it is a parameter,

  • it is not forward-declared, or

  • it is forward-declared and is definitely initialized by the sequence of statements from its declaration to the given statement or declaration.

If a value declaration is definitely initialized at a certain statement or declaration if it is referenceable at that statement or declaration and:

  • it is a parameter,

  • it is not forward-declared and the given statement or declaration is not the value declaration itself, and does not occur within the body of the value declaration, or

  • it is forward-declared and is definitely initialized by the sequence of statements from its declaration to the given statement or declaration.

A function or value declaration must be definitely initialized wherever any value reference or callable reference to it occurs as an expression within the body in which it is declared.

A shared forward-declared declaration belonging to a class and not annotated late must be definitely initialized:

  • at every return statement of the initializer of the containing class, and

  • at the very last expression statement, directive statement or specification statement of the initializer of the containing class.

A specification statement for a method or non-variable reference, getter, or function may not (indirectly) occur in a for or while block unless the declaration itself occurs within the same for or while block.

TODO: Furthermore, the typechecker does some tricky analysis to determine that code like the following can be accepted:

Boolean minors;
for (p in people) {
    if (p.age<18) {
        minors = true;
        break;
    }
}
else {
    minors = false;
}

5.2.6. Definite uninitialization

A sequence of statements may possibly initialize a forward-declared declaration.

  • A sequence of statements possibly initializes a declaration if one of the statements is a specification statement for the declaration or a control structure that possibly initializes the declaration.

  • An if conditional possibly initializes a declaration if either the if block possibly initializes the declaration and the condition list is not never satisfied, or if the else block, if any, possibly initializes the declaration and the condition list is not always satisfied.

  • A switch conditional possibly initializes a declaration if one of the case blocks possibly initializes the declaration or the else block, if any, possibly initializes the declaration.

  • A for loop possibly initializes a declaration if the for block possibly initializes the declaration or if it has an else block that possibly initializes the declaration.

  • A while loop possibly initializes a declaration if the while block possibly initializes the declaration and the condition list is not never satisfied.

  • A try/catch exception manager possibly initializes a declaration if the try block possibly initializes the declaration, if one of the catch blocks possibly initializes the declaration, or if the finally block possibly initializes the declaration.

A forward-declared declaration is considered definitely uninitialized at a certain statement or declaration if:

  • it is not possibly initialized by the sequence of statements from its declaration to the given statement or declaration,

  • the statement does not (indirectly) occur in the for block or else block of a for loop with a for block that possibly initializes it,

  • the statement does not (indirectly) occur in the while block of a while loop with a while block that possibly initializes it,

  • the statement does not (indirectly) occur in a catch block of a try/catch exception manager with a try block that possibly initializes it, and

  • the statement does not (indirectly) occur in the finally block of a try/catch exception manager with a try block or catch block that possibly initializes it.

A function or non-variable value declaration must be definitely uninitialized wherever any value reference or callable reference to it occurs as a specification statement within the body in which it is declared.

5.3. Control structures and assertions

Control of execution flow may be achieved using control directives and control structures. Control structures include conditionals, loops, and exception management.

Ceylon provides the following control structures:

  • the if/else conditional—for controlling execution based on a boolean condition, type condition, or check for a non-null or non-empty value,

  • the switch/case/else conditional—for controlling execution using an enumerated list of values or types,

  • the while loop—for loops which terminate based on a boolean condition, type condition, or check for a non-null or non-empty value,

  • the for/else loop—for looping over elements of an iterable object, and

  • the try/catch/finally exception manager—for managing exceptions and controlling the lifecycle of objects which require explicit destruction.

ControlStructure: IfElse | SwitchCaseElse | While | ForFail | TryCatchFinally | Dynamic

Control structures are not considered to be expressions, and therefore do not evaluate to a value. However, comprehensions—and conditional expressions, planned for a future release of the language—are part of the expression syntax and share much of the syntax and semantics of the control structures they resemble.

Assertions are runtime checks upon program invariants, or function preconditions and postconditions. An assertion failure represents a bug in the program, and is not considered recoverable. Therefore, assertions should not be used to control "normal" execution flow.

Note: of course, in certain circumstances, it is appropriate to handle the exception that results from an assertion failure, for example, to display a message to the user, or in a testing framework to aggregate and report the failures that occurred in test assertions. A test failure may be considered "normal" occurrence from the point of view of a testing framework, but it's not "normal" in the sense intended above.

5.3.1. Control structure variables

Assertions and some control structures allow inline declaration of a variable. A variable is a reference, as defined by §4.8.1 References.

TypedVariable: Type MemberName

In most cases, the explicit type be omitted.

Variable: (Type | "value")? MemberName

If the explicit type is missing from the declaration, the type of the variable is inferred, according to rules that depend upon the control structure to which the variable belongs.

A variable declared by an assertion is a reference scoped to the body in which the assert statement occurs.

A variable declared by a control structure is a reference scoped to the block that immediately follows the variable declaration:

  • For a variable in an if condition, the scope of the variable is the if block.

  • For a variable in a while condition, the scope of the variable is the while block.

  • For a variable in a for iterator, the scope of the variable is the for block.

  • For a variable in a try clause, the scope of the variable is the try block.

  • For a variable in a catch clause, the scope of the variable is the catch block.

  • For a variable in an assert statement, the scope of the variable is the body containing the assert statement.

5.3.2. Iteration variables

A for loop requires an iteration variable declaration. An iteration variable is a reference scoped to the body of the loop.

IteratorVariable: Variable | EntryVariablePair

An iteration variable of type Entry may be specified in destructured form.

EntryVariablePair: Variable "->" Variable

If the type is missing from the declaration, the type of the iteration variable is inferred:

  • given an iterated expression which has the principal instantiation Iterable<X>, the inferred type of the variable is X, unless

  • the destructured form is used for an iterated expression which is has the principal instantiation Iterable<Entry<X,Y>>, in which case the inferred type of the first variable is X, and the inferred type of the second variable is Y.

TODO: Should we, purely for consistency, let you write for (f(Float x) in functions), even though it's not very useful?

5.3.3. Control structure conditions

Some control structures expect conditions. There are four kinds of condition:

  • a boolean condition is satisfied when a boolean expression evaluates to true,

  • an assignabilty condition is satisfied when an expression evaluates to an instance of a specified type,

  • an existence condition is satisfied when an expression evaluates to a non-null value, and

  • a nonemptiness condition is satisfied when an expression evaluates to a non-null, non-empty value.

Condition: BooleanCondition | IsCondition | ExistsOrNonemptyCondition

A condition list has one or more conditions.

ConditionList: "(" Condition ("," Condition) ")"

A condition in the list may refer to a condition variable defined earlier in the list.

A condition list is considered to be always satisfied if every condition in the list is always satisfied. A condition list is considered to be never satisfied if some condition in the list is never satisfied.

TODO: are we going to support satisfies conditions on type parameters, for example, if (Element satisfies Object), to allow refinement of its upper bounds?

5.3.4. Boolean conditions

A boolean condition is just an expression.

BooleanCondition: Expression

The expression must be of type Boolean.

A boolean condition is considered to be always satisfied if it is a value reference to true. A boolean condition is considered to be never satisfied if it is a value reference to false.

TODO: Should we do some more sophisticated static analysis to determine if a condition is always/never satisfied?

5.3.5. Assignability, existence, and nonemptiness conditions

An assignability, existence, or nonemptiness condition may contain either:

  • an unqualified value reference to a non-variable, non-default reference, or

  • an inline variable declaration together with an expression.

In the case of an assignability or existence condition, the type of the variable may be inferred.

IsCondition: "!"? "is" (TypedVariable Specifier | Type MemberName)
ExistsOrNonemptyCondition: ("exists" | "nonempty") (Variable Specifier | MemberName)

TODO: are we going to allow is Type this and is Type outer to narrow the type of a self reference?

The type of the value reference or expression must be:

  • in the case of an assignability condition, a type which is not a subtype of the specified type, but whose intersection with the specified type is not exactly Nothing, except

  • in the case of a negated assignability condition with !is, a type whose intersection with the specified type is not exactly Nothing, and which is not a supertype of the specified type, or

  • in the case of an exists condition, a type whose intersection with Null is not exactly Nothing and whose intersection with Object is not exactly Nothing, or

  • in the case of a nonemptiness condition, a subtype of Anything[]? whose intersection with [] is not exactly Nothing, and whose intersection with [Nothing+] is not exactly Nothing.

Note: an assignability condition may narrow to an intersection or union type.

if (is Printable&Identifiable obj) { ... }
if (is Integer|Float num) { ... }

Every existence or nonemptiness condition is equivalent to—and may be considered an abbreviation of—an assignability condition:

  • exists x is equivalent to is Object x, and

  • nonempty x is equivalent to is [E+] x where x is an expression whose type has the principal instantiation E[]?.

For an is assignability condition:

  • if the condition contains a value reference, the value will be treated by the compiler as having type T&X where the conditional expression is of type T and X is the specified type, inside the block that immediately follows the condition, unless

  • it is a negated assignability condition with !is, in which case the value will be treated by the compiler as having type T~X.

Where, for any given types T and X, the type T~X is determined as follows:

  • if X covers T, as defined by §3.4.1 Coverage, then T~X is Nothing,

  • if T is an intersection type, then T~X is the intersection of all U~X for every type U in the intersection,

  • if T is a union type, then T~X is the union of all U~X for every type U in the union,

  • if T is an enumerated type or an instantiation of a generic enumerated type, then T~X is the union of all C~X for every case C of T, or,

  • otherwise, T~X is T.

If you prefer, you can think of the following:

Transaction tx = ...
if (is Usable tx) { ... }

As an abbreviation of:

if (is Transaction&Usable tx = tx) { ... }

Where the tx declared by the condition hides the outer declaration of tx inside the block that follows.

For an exists existence condition:

  • if the condition declares a variable, the declared type of the variable must be a supertype of T&Object, where the specifier expression is of type T, and must be a subtype of Object, or

  • if the condition contains a value reference, the value will be treated by the compiler as having type T&Object inside the block that immediately follows the condition, where the conditional expression is of type T.

For a nonempty nonemptiness condition:

  • if the condition declares a variable, the declared type of the variable must be a supertype of T&[E+], where the specifier expression is of type T and T has the principal instantiation E[]?, and must be a subtype of [Anything+], or

  • if the condition contains a value reference, the value will be treated by the compiler as having type T&[E+] inside the block that immediately follows the condition, where the conditional expression is of type T and T has the principal instantiation E[]?.

If you prefer, you can think of the following:

if (exists name) { ... }

As an abbreviation of:

if (exists String name = name) { ... }

Where the name declared by the condition hides the outer declaration of name inside the block that follows.

As a special exception to the above, if a condition occurs in a dynamic block, and conditional expression has no type, and the condition contains a value reference, then:

  • the value will be treated by the compiler as having type X where X is the specified type, inside the block that immediately follows the condition, unless

  • it is a negated assignability condition !is, an existence condition exists, or a nonempty condition nonempty, in which case the value will be treated by the compiler as having no type.

5.3.6. if/else

The if/else conditional has the following form:

IfElse: If Else?
If: "if" ConditionList Block
Else: "else" (Block | IfElse)

Every if/else conditional construct has an if clause. The construct may optionally include:

  • a chain of an arbitrary number of child else if clauses, and/or

  • an else clause.

if (payment.amount <= account.balance) {
    account.balance -= payment.amount;
    payment.paid = true;
}
else {
    throw NotEnoughMoneyException();
}
shared void welcome(User? user) {
    if (exists user) {
        print("Welcome back, ``user.name``!");
    }
    else {
        print("Welcome to Ceylon!");
    }
}
if (is CardPayment p = order.payment, 
        !p.paid) {
    p.card.charge(total);
}

5.3.7. switch/case/else

The switch/case/else conditional has the following form:

SwitchCaseElse: Switch Cases
Switch: "switch" "(" Expression ")"
Cases: CaseItem+ DefaultCaseItem?
CaseItem: "case" "(" Case ")" Block
DefaultCaseItem: "else" Block

Every switch conditional construct has a switch clause. The construct must include:

  • a chain of an arbitrary number of child case clauses, and/or

  • an else clause.

Each case is either:

  • a value case—a list of string literals, character literals, integer literals, negated integer literals, and/or value references to anonymous classes, or

  • a type case—an assignability condition of form is V for some type V.

Case: CaseValue ("|" CaseValue)* | "is" Type
CaseValue: LiteralCase | BaseExpression
LiteralCase: "-"? IntegerLiteral | CharacterLiteral | StringLiteral | VerbatimStringLiteral

Every case has a type:

  • for a value case, the type is the union of the types of the values, and

  • for a type case, the type is the specified type.

The type of a case must be a subtype of the switch expression type.

For a value case, each value reference must be to an anonymous class that is a subtype of Identifiable|Null.

For a type case of type V, the intersection type V&U must not be exactly Nothing.

Two cases are said to be disjoint if the intersection of their types is exactly Nothing, as defined by §3.4.4 Disjoint types, or if they are both value cases with distinct literal values. In every switch statement, all cases must be mutually disjoint.

A switch is exhaustive if there are no literal values in its cases, and the union type formed by the types of the cases of the switch covers the switch expression type, as defined by §3.4.1 Coverage.

If no else block is specified, the switch must be exhaustive.

Note: On the other hand, even if the switch is exhaustive, an else block may be specified, in order to allow a switch that accommodates additional cases without resulting in a compilation error.

As a special exception to the above, if a switch occurs in a dynamic block, and the switch expression has no type, the cases are not statically type-checked for exhaustion.

Note: an assignability condition case may narrow to an intersection or union type.

case (is Persistent & Serializable) { ... }
case (is Integer | Float) { ... }

If a switch has an assignability condition case, then the switch expression must be an unqualified value reference to a non-variable, non-default reference.

For an assignability condition case, the value referred by the switch expression will be treated by the compiler as having the intersection type of its declared type with the specified type inside the case block. This intersection type must not be exactly Nothing.

As a special exception to the above, if a switch occurs in a dynamic block, and the switch expression has no type, the value referred by the switch expression will be treated by the compiler as having the the specified type inside the case block.

Boolean? maybe = ... ;
switch (maybe) 
case (null | false) {
    return false;
}
case (true) { 
    return true;
}
Integer|Float number = ... ;
switch (number)
case (is Integer) { 
    return sqrt(number.float);
} 
case (is Float) { 
    return sqrt(number);
}

A Java-style overloaded method may be emulated as follows:

shared void print<Printable>(Printable printable) 
        given Printable of String | Integer | Float {
    switch (printable)
    case (is String) { 
        print("\"``printable``\""); 
    }
    case (is Integer) { 
        print(printable + ".00"); 
    }
    case (is Float) { 
        print(formatFloat(printable, 2)); 
    }
}

5.3.8. for/else

The for/else loop has the following form:

ForFail: For Fail?
For: "for" ForIterator Block
Fail: "else" Block

Every for/else conditional construct has an for clause. The construct may optionally include an else clause, as specified in §8.3.4 Execution of loops.

The for iterator consists of an iteration variable declaration and an iterated expression that contains the range of values to be iterated.

ForIterator: "(" IteratorVariable "in" Expression ")"

The type of the iterated expression depends upon the iteration variable declarations:

  • The iterated expression must be an expression of type assignable to Iterable<X> where X is the declared type of the iteration variable.

  • If two iteration variables are defined, the iterated expression type must be assignable to Iterable<Entry<U,V>> where U and V are the declared types of the iteration variables.

As a special exception to the above, if a for occurs in a dynamic block, and the iterated expression has no type, the iterator is not statically type-checked. If the iteration variable does not declare an explicit type, the iteration variable has no type.

for (p in people) { 
    print(p.name);
}
variable Float sum = 0.0;
for (i in -10..10) {
    sum += x[i] else 0.0;
}
for (word -> freq in wordFrequencyMap) { 
    print("The frequency of ``word`` is ``freq``."); 
}
for (p in group) {
    if (p.age >= 18) {
        log.info("Found an adult: ``p.name``.");
        break;
    }
}
else {
    log.info("No adult in group.");
}

5.3.9. while

The while loop has the form:

While: LoopCondition Block

The loop condition list determines when the loop terminates.

LoopCondition: "while" ConditionList

TODO: does while need an else block? Python has it, but what is the real usecase?

variable Integer n=0;
variable [Integer*] seq = [];
while (n<=max) {
    seq=seq.withTrailing(n);
    n+=step(n);
}

5.3.10. try/catch/finally

The try/catch/finally exception manager has the form:

TryCatchFinally: Try Catch* Finally?
Try: "try" ("(" Resource ("," Resource) ")")? Block
Catch: "catch" "(" Variable ")" Block
Finally: "finally" Block

Every try conditional construct has a try clause. The construct may optionally include:

Each catch block defines a variable. The type of the variable must be assignable to Throwable in ceylon.language. If no type is explicitly specified, the type is inferred to be Exception.

Note: a catch block type may be a union or intersection type:

catch (NotFoundException|DeletedException e) { ... }

If there are multiple catch blocks in a certain control structure, then:

  • The type of a catch variable may not be a subtype of any catch variable of an earlier catch block belonging to the same control structure.

  • If the type of a catch variable is a union type E1|E2|...|En then no member Ei of the union may be a subtype of any catch variable of an earlier catch block belonging to the same control structure.

The try block may have a list of resource expressions, each of which may produce either:

  • a destroyable resource, or

  • an obtainable resource.

Resource: Expression | Variable Specifier

A destroyable resource expression is:

  • an instantiation expression, or

  • an inline variable declaration together with an instantiation expression.

The instantiation expression must be of type assignable to Destroyable in ceylon.language.

An obtainable resource expression is:

  • an expression, or

  • an inline variable declaration together with an expression.

The expression must be of type assignable to Obtainable in ceylon.language.

If no type is explicitly specified for a resource variable, the type of the variable is inferred to be the type of the expression.

try (File(path).lock) {
    file.open(write);
    ...
}
catch (FileNotFoundException fnfe) {
    print("file not found: ``path``");
}
catch (FileReadException fre) {
    print("could not read from file: ``path``");
}
finally {
    assert (file.closed);
}
try (Transaction()) {
    try (s = Session()) {
        return s.get(Person, id);
    }
    catch (NotFoundException|DeletedException e) {
        return null;
    }
}

5.3.11. Assertions

An assertion has an asserted condition list and, optionally, an annotation list.

Assertion: Annotations "assert" ConditionList ";"

The message carried by the assertion failure may be specified using a doc annotation.

"total must be less than well-defined bound"
assert (exists bound, total<bound);

If the assertion contains an assignability, existence, or nonemptiness condition containing a value reference then the compiler treats the referenced value as having a narrowed type at program elements that occur in the lexical scope of the assertion.

{Element*} elements = ... ;
assert (nonempty elements);
Element first = elements.first;

TODO: how can we support interpolation in the assertion failure message?

assert (total<bound) 
else "total must be less than ``bound``";

5.3.12. Dynamic blocks

A dynamic block allows interoperation with dynamically typed native code.

Dynamic: "dynamic" Block

Inside a dynamic block an expression may have no type, as specified in Chapter 6, Expressions.

An expression with no type:

Furthermore:

These situations result in dynamic type checking, as defined in §8.3.6 Dynamic type checking, since the usual static type checks are impossible.

Note: within a dynamic block, Ceylon behaves like a language with optional static typing, performing static type checks where possible, and dynamic type checking where necessary.