Note: All of this capability is implemented except for '.[]' indexing and the '<-' Prefix Operator.
A block is the basic unit for structured control flow. It holds a ordered collection of statements that are executed in order, starting with the first statement. It may also accumulate its own temporary, local state. Blocks that evaluate to some value may also be used in expressions.
Before describing blocks, let's first cover statements.
Statements
A statement is typically some expression to be performed. If the expression evaluates to some value, it is then usually discarded by the statement. In addition to expression statements, there are other kinds of statements, such as variable declarations, a block treated as a statement, or various statements used to control execution flow.
Most statements may be explicitly terminated with a semi-colon. However, specifying the semi-colon is not needed when the statement finishes at the end of a line, as is typically the case. This example show two expression statements:
mut a = 12 // Equivalent to: mut a = 12; mut b = 5 // Equivalent to: mut b = 5;
A single line may hold more than one statement. When this is done, use semi-colons to separate the statements:
a = 24; b = 6 // Equivalent to: a = 24; b = 6;
A statement may span multiple lines. When this is required, continuation lines should be indented to visually signal that all the lines constitute a single statement.
a = b + c + d - e + f // Equivalent to a = b + c + d - e + f;
A failure to indent continuation lines may sometimes lead the compiler to incorrectly believe the continuation line begins a new statement, rather than continuing one already in progress:
a = b + c + d // Equivalent to: a = b + c + d; - e + f // Equivalent to: - e + f;
Blocks
Blocks offer a versatile, easy-to-apprehend way to structure logic. A block's well-defined single point of entry and exit establishes clear boundaries around control flow as well as the local context it manages.
Versatility comes from being able to nest different kinds of block easily within each other. This inherent modularity is useful when used in conjunction with control flow statements or to establish a context for compiler operations (e.g., safety constraints).
Blocks support these roles:
- Holding an ordered collection of statements
- Establishing a lexical scope for a local context
- Building and/or returning a value
Ordered Collection of Statements
At its simplest, a block is merely a ordered set of statements which are executed one after another, in order. There are two ways to mark that several statements belong to the same block:
- Wrap the block's statements inside curly braces:
{ a = 12 b = 5 }
- When a statement ends with a block, signal the start of the block
with a colon, and then indent all of the block's statements which follow on separate lines.
if a == 0: a = 12 b = 5 // Not part of if's inner block, since it is not indented.
This convention makes more efficient use of vertical space, particularly for small blocks.
Blocks may be specified within blocks:
{ ++a; { ++b; --a; } a + b; }
The statements incrementing b and decrementing a comprise an interior block within an outer block. This particular example is a bit contrived, as there is no good reason for us to use an interior block here, since the logic would work the same if all statements belong to a single block. However, later we will see useful examples of blocks embedded within other blocks.
Lexical Context
Execution of a block's statements always begins with the first statement, as one cannot jump into the middle of a block. Likewise, execution of the block always finishes at the end of the block. This single-entrance and single-exit nature makes it possible for a block to create a temporary execution context that persists for the lifetime of the block and then is automatically cleaned up at the end of the block.
Local Variables
Any variable declared wihin a block provides a temporary working state for that block. Such variables are local to their block. They cannot be referenced or used outside of that scope. This silly example illustrates the block scope of declared variables:
{ mut result = 0 { mut sum = a + b result = sum } ++result // sum cannot be referenced in outer block }
The block where a variable is defined is considered to be its lexical scope. Scopes are nested from outer to inner, with global variables having the outer-most scope. Variables declared in outer scopes are accessible in any inner scope. However, the reverse is not true. Thus, the outer block may not refer to sum.
If an inner block declares a local variable of the same name as an outer scope (including global variables), the outer-scoped variable is effectively inaccessible throughout the lexical scope of that inner block. To avoid potential confusion, this should be avoided.
Resource Disposal
The execution lifetime of a local variable does not last beyond its lexical scope. At the end of the block, any resources acquired by local variables are automatically reclaimed. Reclamation might encompass a wide variety of possible activities, including: freeing heap-allocated memory, decrementing counters, releasing locks, joining threads, or other type-based finalization activity that closes acquired resources or removes dependencies to other objects.
Blocks as expressions
Blocks that evaluate to a value may be used as an expression, embedded within a larger expression. When a block is used as an expression, its value is that of the last statement in the block.
// Using a block as an expression a = { 3.14 6 // a's value will become 6 }
with blocks and this
A with block is a special kind of block able to focus its logic around a single value. Its sugar offers a concise way to invoke many methods on, or access fields belonging to, some value. Although it may resemble method chaining or cascades in other languages, it is more versatile.
A this block's simple mechanism consists of two aspects:
- implicit definition of a this variable within a block (not to be confused with self.)
- using operators that implicitly rely on the this variable
Implicit this Variable
A with block is simply an expression followed by a code block. Throughout the block, the variable this is understood to hold the evaluated value of that expression.
// Calculate the value of 'this' (0.174533) with Float.Pi/180: quarter = 90f * this // Use 'this' to convert to radians acute = 10f * this
The above example is short-hand for:
{ imm this = Float.Pi/180 quarter = 90f * this // Use 'this' to convert to radians acute = 10f * this }
Since with blocks can be nested within each other, the value of this for any statement refers to the inner-most with block's expression's value.
'.' Prefix Operator
The convenience of a with block comes from using operators which implicitly work with the value of this. The most common of these is the dot ('.') operator, used to call methods or access fields.
Normally, the '.' operator specifies some data object to the left, on which a method might be invoked or whose field is accessed. However, if no object is specified to the left of '.', this is assumed. It is common to find with block logic taking advantage of this shortcut:
// Normalize point to unit length with &mut point: imm len = (.x * .x + .y * .y).sqrt if len > 0: .x /= len .y /= len
There are several things happening in this example:
- The with block works with a mutable reference to point. We do this because the block changes the value of point. If we had not captured a reference, the block would have tried to change the value of this, which is not mutable and which does not survive the block.
- It uses .x and .y to indirectly access fields in point, via the mutable reference held by this (.x is equivalent to this.x). The block's logic is cleaner and easier to read without having to place point ahead of every dot operator.
Why is this approach is more versatile than method chaining or cascades?
- Called methods need not return self, as method chaining requires.
- Method calls may appear at any point in the block logic, instead of having to be sequenced together.
- It supports field access as well as method calls.
'.[]' indexing
'.[]' may be used to access or modify some element of a collection:
with myScene.numbers: .[4] = 1.6 // Equivalent to: myScene.numbers[4] = 1.6 .[6] = 1.9 .sort // Sort the list of numbers
'<-' Prefix Operator
The <- operator adds the element on the right to the collection on the left. If no collection is specified on the left, this is assumed.
// numbers is a vector of floating-point numbers with myScene.numbers: <- 3.4 <- 2.6 <- 1.4 .sort
This appends three floating point numbers to the list of numbers held in myscene.numbers and then re-sorts the numbers list by calling the sort method.
Note: If we just want to append three numbers without sorting:
myScene.numbers <- [ 3.4 2.6 1.4 ]