Control statements - IS4Code/Sona GitHub Wiki

Control statements alter the flow of code and affect the execution of other statements. They are usually found together with normal statements in blocks, but they are also accepted in certain other contexts, such as in collection expressions, or after inline.

do

The do statement takes a single block and executes it. The do keyword is also found as a part of other statements where it separates a block from the rest of the statement.

Syntax

do_statement:
  'do'
    statements
  'end';

if

The if statement evaluates a series of conditions and executes the first branch where the condition is true, or the else branch, if present.

Syntax

if_statement:
  'if' expression 'then'
    statements
  ('elseif' expression 'then'
    statements)*
  ('else'
    statements),
  'end';

while

The while statement repeatedly executes a block in a loop while a condition is true. The condition is re-evaluated upon each entry into the loop. Inside the statement, break and continue may be used to skip the rest of the block and exit the statement or re-enter it from the beginning, respectively.

Syntax

while_statement:
  'while' expression 'do'
    statements
  'end';

A special version of the statement, while true do (or any version with true wrapped in parentheses), can be used to loop forever, unless the loop is interrupted or an exception is thrown. This version may be regarded as terminating.

repeat…until

The repeat…until statement is similar to while, but evaluates the condition after the body is executed, and only exits the loop if the condition is true. It is possible to use variables created within the body in the condition.

Syntax

repeat_statement:
  'repeat'
    statements
  'until' expression;

for…in

The for…in statement iterates over all elements in a given sequence, repeatedly executing the block with a given variable set to each element of the sequence.

Syntax

for_statement:
  'for' variable 'in' (
    expression |
    expression '..' expression ('by' expression)
  ) 'do'
    statements
  'end';

The .. version is preferred when the expression is formed by .., in which case the loop iterates over a range from the left operand to the right one, optionally with a given amount to skip (1 by default). This interpretation is used even when the .. operator would not denote a range (but concatenation) of the operands, in which case wrapping the operation into parentheses is necessary.

switch

The switch statement matches the result of an expression against a set of patterns, denoted by case, entering the first block corresponding to a matching pattern.

Syntax

switch_statement:
  'switch' expression
  ('case' pattern? ('when' expression)? 'do'
    statements)+
  ('else'
    statements)?
  'end';

Each case branch may also have a corresponding when clause, containing an additional condition for successful match. The condition may use variables declared in the pattern. The pattern may be missing, in which case the when clause is mandatory. The statement may end with an else branch, which is entered if no other case was successful. The else branch must be present if other branches do not handle all cases of the input.

Within a case or else branch, the continue statement together with an expression may be used to match a different value against the same patterns, re-entering the statement. Other occurrences of continue or break are an error.

try

The try statement executes a given block and handles exceptions thrown within the block.

Syntax

try_statement:
  'try'
    statements
  ('case' pattern ('when' expression)? 'do'
    statements)*
  ('else'
    statements)?
  ('finally'
    statements)?
  'end';

The case and else branches have the same interpretation as in switch, but match the exception raised by the main body of the statement. The finally branch is executed in all situations upon leaving the statement, regardless of an exception being thrown. Unhandled exceptions are thrown outside of the statement.

At least one of the case, else, or finally branches must be present.

Within try and finally, the break and continue statements are permitted and control of the enclosing statement that supports them. Within case or else, only the continue statement with an expression is permitted, accepting an Exception value.

Syntactic categories

A control statement can be syntactically placed in a specific category based on all execution paths going through the statement, including all the immediate statements following it (the trailing statements):

  • Terminating, if all paths contain throw or another terminating statement, and do not go through a try statement that may catch the exception.
  • Interrupting, if some paths contain break or continue or another interrupting statement, unless the path goes through a control statement to which the interruptible statement pertains, and all other paths contain a terminating statement.
  • Returning, if some paths contain return or another returning statement, and all other paths contain a terminating or interrupting statement.
  • Interruptible, if some paths contain an interrupting statement and at least one other path is open, or some paths contain an interruptible statement. The other paths are open or contain a terminating statement.
  • Conditional, if some paths contain a returning statement and among the other paths at least one is not terminating, or some paths contain a conditional statement.
  • Free, if not in any other category.

The recursive nature of these rules is important to be able to bypass syntactic restrictions on normal statements, for example throw and do throw end are both considered terminating.

The final category of a statement can be also determined by first setting up all paths going through it as terminating, and gradually replacing any terminating path with one of a different kind. This is illustrated in the following image; O corresponds to adding an open path, R to a returning path, and I to an interrupting path:

/images/Control-statements-categories.svg

The overall distinction is necessary to maintain because F# does not support exiting a block natively, and so all such blocks need to be rewritten in a semantically equivalent form without any unsupported statements:

  • Free statements do not affect the flow of code in any way, and do not require any special behaviour.
  • The trailing statements of returning, interrupting, and terminating statements are ignored. While they are treated during compilation as any other piece of code, they do not participate in any relevant execution paths.
  • Terminating statements are regarded as able to produce a value of any type (by virtue of never returning). A function ending with a terminating statement is thus also able to produce a value of any type. They also act as a sort of "wildcard", by acting as returning or interrupting when necessary.
  • Returning statements always produce a single value. A function ending with a returning statement is thus able to get its value from it directly.
  • Interrupting statements affect the behaviour of enclosing statements, and require similar handling as returning statements, but do not have effect outside the control statement they relate to. Importantly, mixing returning and interrupting statements makes the whole statement returning ‒ the reason is that return already needs to interrupt a block and so covers everything break must be able to do, while interrupting statements do not affect outside statements when contained in control statements they affect, unlike return.
  • Interruptible and conditional statements are the open versions of interrupting and returning statements, respectively, containing at least one open path.

Note that the placement of a statement in one of these categories depends on syntactical analysis, not semantic. This means that the mere presence/absence of return, throw or similar within the relevant parts of the statement is the sole deciding factor, not information that could be derived from types, constants, or similar.

Examples

if c then
  print(x)
end

The if is a free statement, since there are no special statements that would affect its category.

if c then
  return x
end

This if is a conditional statement, since one path through it (when not c) does not go through the return.

if c then
  return x
end
return y

This if is a returning statement. Even though the if itself did not change, the non-returning path is now returning thanks to the trailing return.

if c then
  throw x
end
return y

This if could be considered a free statement or a returning statement, since the trailing statements are returning. Regardless, the semantics of the whole block are the same.

if c then
  throw x
else
  throw y
end
return z

This if is a throwing statement. The trailing return is ignored.