Statements

A statement is usually something which ends with a semicolon, but there are exceptions to this rule.

Variable Declaration

This type of statement declares one or more variables to be of a given type or list of types. It can be written as follows:

[final] <type> <declaration-assignee> [ , <declaration-assignee> ] ;

Where a declaration assignee is:

<name> | _

If multiple assignees are given, they are usually all given the specified type. However, if the type is a non-nullable tuple of X other types, and there are X assignees, then each assignee will be assigned the corresponding type from inside the tuple (i.e. the first variable will get the first type, the second will get the second, etc.). For example:

uint x, y, z; // x, y, and z are all uints
(string, uint, double) a, b, c; // a: string, b: uint, c: double
(string, uint, double) d, e, f, _; // d: (string, uint, double), e: (string, uint, double), f: (string, uint, double)

Assignment

An assignment is very similar to a variable declaration, but it also assigns values to its assignees. It can also have more different types of assignee. The following is a simplified version of its grammar:

AssignStatement = OptionalModifiers Type AssigneeList Equals Expression ;
AssigneeList = Assignee | AssigneeList Comma Assignee
Assignee = Name | Primary [ Expression ] | Primary . Name | Type :: Name | _

The only valid modifier for an assignment is final, which applies to all new local variables created with the assignment, and stops them from being modified again after they are first assigned to. A Primary is an expression that does not include any unary or binary operators at the top level.

The possible assignees are:

  • Local variables
  • Fields and Properties of this object (both static and non-static)
  • Property backing variables (if inside a property method)
  • Array elements
  • Fields and properties of another object
  • Static fields and properties of another type
  • The blank assignee (underscore) which does nothing

If a type is provided, at least one of the assignees must be a new local variable which is being declared with this assignment. If multiple assignees are given, the rules used to determine their types are the same as in variable declarations. It is an error to provide a type which does not match the actual type of an assignee.

During execution, each of the assignees is evaluated in turn, followed by the expression. The expression is then assigned to the assignees. If there are multiple assignees, then the expression must result in a not-null tuple which can be split into the assignees’ types.

If a property is assigned to (and it is not a property backing variable), then the property’s setter or constructor will be called (depending on the location of the assignment) instead of just writing to a variable.

In many other languages, assignments are expressions rather than statements. In Plinth, the decision to make them pure statements was made to avoid the often-unnecessary complexity of allowing local variables to change mid-expression. This decision also makes it impossible to accidentally do an assignment where an equality check was intended, as is easy to do in C with if (c = 1).

Block

A block is simply a list of other statements, which are executed in order. It can be written as follows:

{
  // Arbitrary statements go here.
}

Some statements, such as if, for, and try, are made up partly of blocks.

Break

This can only be used during breakable statements such as loops and switch statements. It stops execution at the current point and continues immediately after a given breakable statement. It can be written as follows:

while true {
  break;
}
// Execution carries on here.

It can also break out of multiple statements at a time, but does not break out of normal blocks or other kinds of statement:

while true {
  {
    while true {
      break 2;
    }
  }
}
// Execution carries on here.

Continue

Similarly to break, this can only be used during loops. It stops execution at the current point in the loop and continues just before the loop check is executed. It can be written as follows:

while i < 5 {
  if i == 0 {
    continue;
  }
  ++i;
}
// If i == 0, this loop continues forever.

Like break, it can also continue through multiple breakable statements at a time:

// Execution continues just before this loop check.
while i > 2 {
  {
    while i < 5 {
      if i == 3 {
        continue 2;
      }
    }
  }
}

Delegate Constructor

This can only be used during the constructor of a class or value type. The rules about exactly when these can be called are relatively complicated, and are outlined in the section on Initialisation.

There are two different forms of delegate constructors: super() and this(). Calls to this() delegate constructors will call a constructor from the same type definition, whereas calls to super() delegate constructors will call a constructor from the direct superclass. A simplified version of the LALR(1) grammar for them is as follows:

DelegateConstructorStatement = this Arguments ; | super Arguments ;
Arguments = ( ArgumentList ) | ( )
ArgumentList = Argument | ArgumentList , Argument
Argument = Expression | Name Equals Expression

The constructor to call is resolved using the type that it is called on and types of each of the arguments.

Since this() delegate constructors call constructors from the same type definition, they can be used in constructors of both classes and value types.

Note that it is not possible to call a grandparent class’s constructor directly, as doing so would bypass the direct superclass’s initialisation.

Expression

Some Expressions can also be used as statements: function calls, and creations. A simplified LALR(1) grammar for these ExpressionStatements is as follows:

ExpressionStatement = FunctionCallExpression ;
                    | cast < Type > FunctionCallExpression ;
                    | CreationExpression ;

These work by executing the expression as normal, and not doing anything with the result (if any).

If the function call is difficult to resolve because there are multiple functions with the same name which take the same parameters, then the cast variant can be used to disambiguate the calls by their return types.

For Loops

For loops are very similar to the for loops in other imperative languages. They contain an initialisation clause, a condition, an update clause, and a block of code to execute during the loop. Excluding the block, all of these clauses can be omitted if they are not required. If the condition is omitted, then the only ways to exit the loop are by using a break statement or by throwing an exception. The LALR(1) grammar for a for loop is as follows:

ForInit = AssignStatement | ShorthandAssignment ; | ;
ForUpdate = ++ Assignee | -- Assignee | AssigneeList Equals Expression | ShorthandAssignment | FunctionCallExpression | ε
ForStatement = for ( ForInit Expression ; ForUpdate ) Block
             | for ( ForInit            ; ForUpdate ) Block

The process for executing a for loop is as follows:

  1. The initialiser (if any) is executed.
  2. The condition (if any) is checked. If the check fails, control jumps to immediately after the loop (step 6).
  3. The block (or loop body) is executed.
  4. The update (if any) is run.
  5. The process jumps back to step 2, and the condition is checked again.
  6. If the loop condition fails, or the loop is broken out of via a break statement, execution continues with the next statement after the for loop.

If the for loop does not have a condition, and is never broken out of, then it is impossible for execution to continue after it.

For Each Loops

For each loops allow iteration over various sorts of iterable values. The LALR(1) grammar for a for-each loop is as follows:

ForEachStatement = for Modifiers Type Name in Expression Block
                 | for           Type Name in Expression Block

The variable that is defined as part of the for each statement is assigned to each of the values from the iterable in turn, each on a separate iteration of the loop. The only modifier permitted for a for-each variable is final, which means that the variable can never be reassigned inside the loop body.

The process for executing a for each loop is as follows:

  1. The expression is executed, and its result is turned into an iterator.
  2. The iterator is checked for a next value. If this check fails, control jumps to immediately after the loop (step 6).
  3. The next value is extracted from the iterator, and it is assigned to the variable.
  4. The block is executed with this value in the variable.
  5. The process jumps back to step 2, and the iterator is checked for a next value again.
  6. If the iterator runs out of elements, or the loop is broken out of via a break statement, execution continues with the next statement after the loop.

The possible values that can be iterated over are:

  • Unsigned integers: for a value n, the variable takes each of the values from 0 to n-1, inclusive.
  • Signed integers: for a value n, the variable takes each of the values from 0 to n-1 if n is positive, or from 0 to n+1 if n is negative.
  • Arrays: the variable takes each element of the array in turn
  • Objects which implement Iterator<T>: the variable takes values from the iterator until it is exhausted. The iterator is read from its current position onwards, and is never reset.
  • Objects which implement Iterable<T>: the iterator() method is called to produce an Iterator<t>, which is used as above.

If an object inherits from both Iterator<T> and Iterable<T>, then a preferred type is calculated as follows:

  • All types which are incompatible with the for-each loop’s variable are ignored.
  • If an exact match to the variable’s type is found, it is used. If there are two exact matches, one for Iterator and one for Iterable, then the Iterator is used.
  • If no exact matches are found, then the supertypes are searched in the order of the type’s linearisation, and the first compatible Iterator or Iterable is used.

If Statements

If statements allow the result of a boolean expression to decide whether to execute a block of code. They may optionally contain an else clause, which is executed iff the first block is not executed. The LALR(1) grammar for an if statement is as follows:

IfStatement = if Expression Block | if Expression Block else Block | if Expression Block else IfStatement

The main difference between the if statement in Plinth and the if statements in C, C++, C#, and Java, is that in Plinth parentheses are not required around a conditional, and blocks are required as part of the statement (there is no short-if in Plinth). To allow for else-if continuations, the language specifically allows another if statement to be provided instead of an else block, as follows:

if a == b {
  stdout::println("a == b");
} else if c == d {
  stdout::println("c == d");
} else {
  stdout::println("a != b && c != d");
}

The if c == d ... is a separate if statement that is only executed if the a == b check evaluates to false.

The condition of an if statement must always be of type boolean. Any other types, such as uint or ?boolean, will result in a compiler error.

Increment and Decrement statements

In Plinth, the increment and decrement (++ and --) operators are not expressions as they are in many other languages. As with assignments, this decision was made because code is usually much easier to understand when variables cannot change value mid-statement, and also to avoid the possibility of confusion between the semantics of the prefix and postfix versions. In Plinth, only the prefix version of the operators exist (TODO: change to postfix).

The LALR(1) grammar for increment and decrement statements is as follows:

IncDecStatement = ++ Assignee ; | -- Assignee ;

The assignees here are exactly the same ones allowed in assignment statements. The only extra constraint this statement adds is that the type of the thing being incremented or decremented must be a non-nullable integer or floating point type (i.e. one of byte, ubyte, short, ushort, int, uint, long, ulong, float, and double).

The increment statement always increases the value by 1, while the decrement always decreases the value by 1. For integer types, wraparounds are allowed and do not cause any undefined behaviour. For floating-point types, it is possible that the operation can have no effect, either because the value is too large or because it represents a value such as NaN or one of the infinities.

Return Statements

A return statement stops execution of a function and (depending on the function’s return type) specifies the result to the caller. The LALR(1) grammar for it is as follows:

ReturnStatement = return Expression ; | return ;

The return statement is only permitted inside methods and constructors, never inside (static or instance) initialisers. If the return type of the method is void or the statement is inside a constructor, then the expressionless version must be used; otherwise, the version with an expression must be used, and the type of the expression must be convertible to the return type of the method.

Shorthand Assignments

Shorthand assignments are a slightly simpler way of writing assignments of the form x = x + y. There are several operators which they can be used with, which each represent an equivalent standard assignment:

Shorthand Assignment
a += b; a = a + b;
a -= b; a = a - b;
a *= b; a = a * b;
a /= b; a = a / b;
a %= b; a = a % b;
a %%= b; a = a %% b;
a <<= b; a = a << b;
a >>= b; a = a >> b;
a &= b; a = a & b;
a |= b; a = a | b;
a ^= b; a = a ^ b;

The LALR(1) grammar for a shorthand assignment is:

ShorthandAssignment = AssigneeList +=  Expression
                    | AssigneeList -=  Expression
                    | AssigneeList *=  Expression
                    | AssigneeList /=  Expression
                    | AssigneeList %=  Expression
                    | AssigneeList %%= Expression
                    | AssigneeList <<= Expression
                    | AssigneeList >>= Expression
                    | AssigneeList &=  Expression
                    | AssigneeList |=  Expression
                    | AssigneeList ^=  Expression

Shorthand assignments can work on more than one assignee at a time. If several assignees are given and the expression results in a not-nullable tuple with the same number of elements as we have assignees, then each assignee is combined with the corresponding element of the expression (via whichever operator is used). On the other hand, if the expression does not result in such a tuple, then the result of the expression is combined with each element in turn (via whichever operator is used). For example:

int a, b = 5, 9;
a, b += 3;       // a = 8, b = 12
a, b /= 4, 2;    // a = 2, b = 6
a, b, _ -= 1, 2; // Compiler error: can't subtract the tuple (1, 2) from anything.

Throw Statements

A throw statement propagates an exception back through the stack to the next catch block that matches it. If any finally blocks are encountered along the way, they are executed as they are passed through.

The LALR(1) grammar for a throw statement is as follows:

ThrowStatement = throw Expression ;

For precise semantics on how exception handling works, see Exception Handling.

Try Statements

Try-catch-finally blocks are exception handling structures that can both catch exceptions and define cleanup code that will run even if an exception is thrown.

The LALR(1) grammar for a try statement is as follows:

CatchTypeList = Type | CatchTypeList Pipe Type
TryCatchStatement = try Block catch           CatchTypeList Name Block
                  | try Block catch Modifiers CatchTypeList Name Block
                  | TryCatchStatement catch           CatchTypeList Name Block
                  | TryCatchStatement catch Modifiers CatchTypeList Name Block
TryFinallyStatement = try Block finally Block
                    | TryCatchStatement finally Block
TryStatement = TryCatchStatement | TryFinallyStatement

Essentially, a try statement must have an initial try block, and then an arbitrary number of catch clauses, followed by an optional finally clause. Either a finally clause or at least one catch clause must be written. Note that, unlike in some other languages with exception handling, no parentheses occur around the header of the catch clause.

The basic semantics of a try statement are that the try statement is executed first, and if any exceptions happen during its execution then the catch blocks are tested for a match to the thrown exception. If any matches are found, the first one is executed. If not, the finally block (if any) is executed before the exception continues propagating up the stack. If the try block or one of the catch block finishes normally, then the finally block is executed and execution continues after the try statement. If an exception is thrown during a catch block then the finally block is executed before the exception continues to propagate up the stack.

Catch clauses can specify multiple exception types for their exception variable. If an exception matches the catch clause, it is assigned to the exception variable and the clause’s block is run. An exception matches a catch clause if it can be converted to any of the types that the catch clause specifies. Note that these are performed as normal run-time type checks, and so parametrised types are fully supported.

For more precise semantics on how exception handling works, see Exception Handling.

While Statements

While statements are the simplest kind of loop in Plinth, they consist only of a conditional and a body. The LALR(1) grammar for a while loop is as follows:

WhileStatement = while Expression Block

When execution reaches a while loop, the condition is executed. If the result of the condition is true then the block is executed, otherwise control continues after the while loop. At the end of the loop body, control jumps back to just before the condition was executed, and the process begins again.

If a break statement which targets this while loop is executed, control jumps to just after this while loop. If a continue statement which targets this while loop is executed, control jumps to just before the condition is executed.