15. Statements

15.1. Assignment Statements

In Gazprea a mutable variable may have different values throughout the execution of the program. Mutable variables may have their values changed with an assignment statement. In the simplest case an assignment statement contains an identifier on the left hand side of an equals sign, and an expression with a compatible type on the right hand side.

var integer x = 7;

x -> std_output;  /* Prints 7 */
'\n' -> std_output;

/* Give 'x' a new value */
x = 2 * 3;  /* This is an assignment statement */

x -> std_output;  /* Prints 6 */

Output

7
6

Type checking must be performed on assignment statements. The expression on the right hand side must have a type that can be implicitly cast to the type of the variable. If it does not, the compiler must emit a TypeError (see Errors). For instance:

var integer int_var = 7;
var real real_var = 0.0;
var boolean bool_var = true;

/* Since 'int_var' is an integer it can be implicitly cast to a real number */
real_var = int_var;  /* Legal */

/* Real numbers cannot be turned into boolean values automatically. */
bool_var = real_var; /* Illegal */

Errors

This program is ill-formed; the compiler must reject it (TypeError).

Assignments can also be more complicated than this with arrays and tuples. With arrays indices may be provided in order to change the value of an array element. As in any indexing context, an array cannot be indexed with an array value (see the indexing rules for the normative statement and its TypeError) and a range written directly inside an index position is not an array-valued index but forms a slice. For instance, with single dimensional arrays:

var integer[*] v = [0, 0, 0];

/* Can assign an entire array value -- change 'v' to [1, 2, 3] */
v = [1, 2, 3];

/* Change 'v' to [1, 0, 3] */
v[2] = 0;
v -> std_output;

Output

[1 0 3]

This applies to arrays of any dimension.

var integer[*][*] M = [[1, 1], [1, 1]];

/* Change the entire matrix M to [[1, 2], [3, 4]] */
M = [[1, 2], [3, 4]];

/* Change a single position of M */
M[1][2] = 7;  /* M is now [[1, 7], [3, 4]] */
M -> std_output;

Output

[[1 7] [3 4]]

Assigning a whole array value changes an array’s contents, never its length. Because an array is initialization-time sized, its length is fixed once at initialization; the right hand side is fitted to that fixed length, with a shorter value padded using the element type’s zero value and a longer value causing the compiler to emit a SizeError (see Errors and Sizing) at compile time or run time. Assigning to a vector behaves differently: it replaces the contents and the length together, so there is no padding and no SizeError.

var integer[*] a = [1, 2, 3];

/* 'a' keeps its fixed length 3; the shorter value is padded with
   the integer zero value, so 'a' becomes [4, 5, 0]. */
a = [4, 5];

/* A longer value cannot fit the fixed length -- SizeError. */
a = [4, 5, 6, 7];  /* SizeError */
var vector<integer> vec = [1, 2, 3];

/* A vector replaces contents and length together, so 'vec'
   becomes [4, 5] with length 2 -- no padding, no SizeError. */
vec = [4, 5];

Tuples also have a special unpacking syntax in Gazprea. A tuple’s field may be assigned to comma separated variables instead of a tuple variable. For instance:

var integer x = 0;
var real y = 0;
var real z = 0;

tuple(integer, real) tup = (1, 2.0);

/* x == 1, and y == 2.0 now */
x, y = tup;

/* Types can be implicitly cast */

/* z == 1.0, y == 2.0 */
z, y = tup;

/* Can swap: z == 2.0, y == 1.0 */
z, y = (y, z);

x -> std_output; '\n' -> std_output;
y -> std_output; '\n' -> std_output;
z -> std_output;

Output

1
1
2

The types of the variables must match the types of the tuple’s fields, or the tuple’s fields must be able to be implicitly cast to the variable’s type; otherwise the compiler must emit a TypeError (see Errors). The number of variables in the comma separated list must match the number of fields in the tuple, if this is not the case the compiler must emit an AssignError (see Errors). This assignment is performed left-to-right. The entire right-hand side is, however, fully evaluated into a temporary before any left-hand-side variable is written; this is what lets a swap such as z, y = (y, z); behave as expected even though the individual writes then happen left-to-right.

Assignments and initializations must perform a deep copy. It should not be possible to cause the aliasing of memory locations with an assignment. For instance:

integer[*] v = [1, 2, 3];
var integer[*] w = v;

w[2] = 0;  /* This must not affect 'v' */

/* v has the value [1, 2, 3] */
/* w has the value [1, 0, 3] */

/* If you are not careful, you might copy the pointer of 'v' to 'w',
   which would cause them to be stored in the same location in memory. If
   this happens modifying 'w' would change 'v' as well.
 */
v -> std_output; '\n' -> std_output;
w -> std_output;

Output

[1 2 3]
[1 0 3]

The above is a simple example using arrays. You must ensure that values cannot be aliased with an assignment between any types, including arrays and tuples.

Binding a slice to a variable, as in const b = a[1..3];, copies the selected elements into a fresh, independent array, so b does not alias a and neither one sees the other’s later writes. A slice writes through to its backing array only when it is the target on the left of an assignment (a[1..3] = [4, 5, 6];). In other words when the slice denotes an lvalue with an offset and length into an array. Everywhere else a slice is an ordinary array value, exactly like an array literal. Every assignment and initialization deep-copies, so, for example, creating a new struct copies the right-hand side and never aliases it through indexing.

Variables may be declared as const, and in this case a program that places them on the left hand side of an assignment statement is ill-formed. The compiler must emit an AssignError (see Errors) on violation.

The right hand side of an assignment statement is always evaluated before the left hand side. This is important for cases where procedures may change variables, for instance:

v[x] = p(x);
/* If p changes x then it is important that p(x) is executed before v[x] */

15.2. Block Statements

A list of statements may be grouped into one statement using curly braces. This is called a block statement, and is similar to block statements in other languages such as C/C++. As an example:

{
  x = 3;
  z = 4;
  x -> std_output; "\n" -> std_output; z -> std_output; "\n" -> std_output;
}

Is a block statement. Declarations may appear anywhere within a block, interleaved with the other statements (see Declarations). In previous versions of the specification, declarations could only appear at the start of the block, this restriction has been removed in gazprea. Each block statement introduces a new scope that new variables may be declared in. For instance this is perfectly valid:

integer x = 3;
var integer y = 0;
var real z = 0;

{
  real x = 7.1;
  z = x;
}

y = x;
y -> std_output; '\n' -> std_output;
z -> std_output;

Output

3
7.1

After execution, y == 3 and z == 7.1.

15.3. If/Else Statements

An if statement takes a boolean value as a conditional expression, and a statement for the body. If the conditional expression evaluates to true, then the body is executed. If the conditional expression evaluates to false then the body of the if statement is not executed. If statements in Gazprea require the conditional expression to be enclosed in parentheses.

The conditional expression must be a scalar boolean. Supplying a non-boolean value, or a boolean array such as if ([true, false]), is a TypeError (see Errors). The same requirement applies to the control expression of a predicated loop.

integer x = 0;
var integer y = 0;

/* Compute some value for x */

if (x == 3) {
   y = 7;
}

/* At this point y will only be 7 if x == 3, and otherwise y will be
   0, assuming it did not change throughout the rest of the program.
 */

If statements are often paired with block statements, like in the above example. The if statement above could also be written as:

if (x == 3)
  y = 7;

Since y = 7; is a statement it can be used as the body statement. All statements after this point are not in the body of the if statement. For instance:

if (x == 3)
  y = 7;
  z = 32;

is actually equivalent to the following:

if (x == 3) {
  y = 7;
}

z = 32;

Gazprea is not sensitive to whitespace, so we could even write something like:

if (x == 3) y = 7;

An if statement may also be followed by an else statement. The else has a body statement just like the if statement, but this is only run if the conditional expression on the if statement fails.

if (x == 3)
  y = 7;
else
  y = 32;

Now if x does not have a value of 3, y is assigned a value of 32. This can be paired with if statements as well.

y = 0;

if (x < 0) {
  y = -1;
}
else if (x > 0) {
  y = 1;
}

/* y is negative if x is negative, positive if x is positive,
  and 0 if x is 0. */

15.4. Loop

Gazprea has a single loop keyword that forms four loop variants, all valid:

  • an infinite looploop <body> with no control expression;

  • a pre-predicated (while-style) loop – loop while (<cond>) <body>;

  • a post-predicated (do-while-style) loop – loop <body> while (<cond>);;

  • an iterator (for-style) loop – loop <var> in <domain> <body>.

Each variant is described below.

15.4.1. Infinite Loop

Gazprea provides an infinite loop, which continuously executes the body statement given to it. For instance:

loop "hello!\n" -> std_output;

Would print “hello!” indefinitely. This is often used with block statements.

/* Infinite counter */
var integer n = 0;

loop {
  n -> std_output; "\n" -> std_output;
  n = n + 1;
}

15.4.2. Predicated Loop

A loop may also be provided with a control expression. The control expression automatically breaks from the loop if it evaluates to false when it is checked.

The loop can be pre-predicated, which means that the control expression is tested before the body statement is executed. This is the same behavior as while loops in most languages, and is written using the while token after the loop, followed by a boolean expression for the predicate. For example:

var integer x = 0;

/* Print 1 to 10 */
loop while (x < 10) {
  x = x + 1;
  x -> std_output; "\n" -> std_output;
}

Output

1
2
3
4
5
6
7
8
9
10

A post-predicated loop is also available. In this case the control expression is tested after the body statement is executed. This also uses the while token followed by the control expression, but it appears at the end of the loop. Post-predicated loop statements must end in a semicolon.

integer x = 10;

/* Since the conditional is tested after the execution '10' is printed */
loop x -> std_output; while (x == 0);

The body may equally be a block statement; the trailing while and its required semicolon are what distinguish a post-predicated loop from a plain infinite loop over a block:

var integer x = 0;

/* Prints 1 to 10; the condition is tested after each pass */
loop {
  x = x + 1;
  x -> std_output; "\n" -> std_output;
} while (x < 10);

Output

1
2
3
4
5
6
7
8
9
10

The single-statement post-predicated form (loop <stmt>; while (cond);) is distinguished from a plain infinite loop whose body is that same statement only by the trailing while, so the two productions can diverge arbitrarily far into the input. ANTLR’s adaptive LL(*) prediction resolves this without special effort, but a hand-written or fixed-lookahead LL(k) grammar will need care around this production.

15.4.3. Iterator Loop

Loops can be used to iterate over the elements of an array of any type, or over a vector or string. This is done by using domain expressions (for instance i in v) in conjunction with a loop statement. In a domain expression x in E, x is the iterator variable and E is the domain.

When the domain is given by an array, each time the loop is executed the next element of the array is assigned to the iterator variable. The elements of the domain array are assigned to the iterator variable starting from index 1, and going up to the final element of the array. When all of the elements of the domain array have been used the loop automatically exits. For instance:

/* This will print 123 */
loop i in [1, 2, 3] {
  i -> std_output;
}

Output

123

Array ranges can also be used instead:

// This will print 123
loop i in 1..3 {
  i -> std_output;
}

Output

123

The domain is evaluated once, when control first reaches the loop; see Domain Expressions for the full evaluate-once and re-initialization semantics of the iterator variable on each pass.

Note that multiple domain expressions are not allowed; the compiler must emit a SyntaxError (see Errors) for an iterator loop with more than one domain expression.

integer[*] u = [1, 2];
integer[*] v = [3, 4];

// This is illegal
loop i in u, j in v {
  "Hello!\n" -> std_output;
}

Errors

This program is ill-formed; the compiler must reject it (SyntaxError).

If you want multiple domains, use a nested loop instead:

integer[*] u = [1, 2];
integer[*] v = [3, 4];

loop i in u {
  loop j in v {
    "Hello!\n" -> std_output;
  }
}

Output

Hello!
Hello!
Hello!
Hello!

15.5. Break

A break statement may only appear within the body of a loop. When a break statement is executed the loop is exited, and Gazprea continues to execute after the loop. This only exits the innermost loop that actually contains the break.

/* Prints a 3x3 square of *'s */
var integer x = 0;
var integer y = 0;

loop while (y < 3) {
  y = y + 1;
  x = 0;   /* reset the column counter at the start of each row */

  /* Normally this would loop forever, but the break exits this inner loop */
  loop {
    if (x >= 3) break;

    x = x + 1;
    "*" -> std_output;
  }

  "\n" -> std_output;
}

Output

***
***
***

If a break statement is not contained within a loop the compiler must emit a StatementError (see Errors).

15.6. Continue

Similarly to break, continue may only appear within the body of a loop. When a continue statement is executed the innermost loop that contains the continue statement starts its next iteration. continue stops the execution of the loop’s body statement, the loop then continues as though the body statement finished its execution normally. If a continue statement is not contained within a loop the compiler must emit a StatementError (see Errors).

/* Prints every number between 1 and 10, except for 7 */
var integer x = 0;

loop while (x < 10) {
  x = x + 1;

  if (x == 7) continue;  /* Start at the beginning of the loop, skip 7 */

  x -> std_output; "\n" -> std_output;
}

Output

1
2
3
4
5
6
8
9
10

15.7. Return

The return statement is used to stop the execution of a function or procedure. When a function/procedure returns then execution continues where the function/procedure was called.

If the function/procedure has a return type then the return statement must be given a value that is the same as or able to be implicitly cast to (see Implicit Casts) the return type; this will be the result of the function/procedure call. If the value is neither, the compiler must emit a TypeError (see Errors). Here is an example:

function square(integer x) returns integer {
  return x * x;
}

procedure main() returns integer {
  square(5) -> std_output;
  return 0;
}

Output

25

A function, and a procedure that has a returns clause, must return a value on every control-flow path. If control can reach the end of the body without executing a return, the program is ill-formed and the compiler must emit a ReturnError (see Errors); see Functions and Procedures for the full rule and examples.

If a procedure has no returns clause, then it has no return type and a return statement is not required but may still be present in order to return early. In this case return is used as follows:

procedure do_nothing() {
  return;
}

15.8. Stream Statements

See Streams for the streams Gazprea provides and their output/input formatting rules. Stream statements are the statements used to read and write values in Gazprea.

Output example:

2 * 3 -> std_output;  /* Prints 6 */

Output

6

Input example:

var integer x;
x <- std_input; /* Read an integer into x */
x -> std_output;

Input

42

Output

42