Blocks were briefly introduced in the function chapter. They provide a modular way to package a function's logic.
There are several kinds of blocks, each with a different approach to supporting these key roles:
- Holding an ordered collection of statements
- Controlling the execution flow
- Establishing a lexical scope for a local context
- Building and/or returning a value
- Establishing how rigorously to enforce safety rules
Blocks offer a versatile, comprehensible way to structure logic. Their well-defined single point of entry and exit simplifies both control flow and context management. Versatility comes from being able to nest different kinds of block easily within each other.
Ordered Collection of Statements
At its simplest, a block is merely a ordered set of statements. You can tell which statements are part of the same block, as they are indented more than the statements that come before and after the block. This indentation typically consists of spaces, although tabs are supported if used consistently. Alternatively, curly braces may be used (rather than indented lines) to enclose a block's statements.
Each statement typically has its own line. However, multiple statements may be placed on the same line, separated with semicolons. Use of the backslash continuation character can be used whenever a line needs to span multiple lines. The detailed styling rules are explained here.
A statement can be:
- An expression. This could be a function call, arithmetic calculation, assignment, or even another nested block (as a block can be an expression that evaluates to a value). The return value of an expression used as a statement is often simply discarded.
- A local variable or function declaration (discussed further below).
- A special-purpose statement or block, typically beginning with a reserved keyword (e.g., return, if or while)
Blocks and Control Flow
Cone supports many different kinds of blocks. From a control flow perspective, these can be categorized as:
- Single use. All of the block's statements are executed just once, in sequence. In addition to the unadorned basic block, this, build and using blocks are also single use.
- Path selection. One or more conditional expressions are used to select which (of several) block's statements to perform. if and match blocks offer path selection.
- Loop. One or more conditional expressions are used to determine how many times a set of statements are repeatedly performed. while and each blocks offer loop control.
- Exception handling. Certain errors will stop the normal execution of a block's statements. Instead, execution is shunted off to exception handling logic.
- Concurrency. Multiple sets of statements can be performed concurrently.
Local variables and the lexical context
A block may carry its own state information in the form of local, named variables. This block state is transient; space for it is effectively allocated when execution of the block begins. When execution of the block ends (by any means), appropriate clean up activity is triggered (where defined) and then the state information disappears.
Local variables are declared exactly the same way as described for global variables. They are known to be local simply because they are declared within a function's block.
fn summult(a f32, b f32) f32 imm sum, mult = a + b, a * b // local variable declaration return sum / mult
Once a local variable has been declared, it may be used anywhere after in the same (or inner) block. However, if an inner block declares a local variable of the same name (allowed but not always good practice), the outer scoped variable is effectively invisible throughout the lexical scope of that inner block.
Blocks use a number of mechanisms under the covers to ensure that resources attached to local variables are handled safely. This is particularly true when references are involved. Such mechanisms enforce aliasing and shared use permission constraints and trigger appropriate allocator de-aliasing events. Further details are covered in the reference chapters.
Building and/or returning a block value
In many cases (depending on the kind of block), a block can be used as an expression.
a = 3.14 6 // a's value will become 6
The value of an expression block is the value of its last expression statement, in effect working exactly the same way as a function's implicit return. As you will see later, the same principle applies to if and match control blocks.
Additionally, a build or this block may be used to focus on and build a value, which is a particularly helpful and readable way to assemble containers.
Enforcing safety rules
The compiler comes with built-in guard rails to help protect your code against a number of safety hazards, which is a good thing. However, these safety guards are extremely cautious and not always very clever. They stop your code from doing things that, under the worst of all possible conditions, could cause memory or race conditions. Sometimes this means they stop your code from doing things that you want to do, and which you can ensure by other means is perfectly safe, but the compiler just does not know how to verify. The compiler is trying to be helpful, but is just getting in the way.
In such situations, Cone provides a built-in escape hatch from the guard rails, in the form of an annotation. By preceding any block with trust (as in: "trust me on this"), several safety checks are turned off, thereby allowing:
- Raw pointer dereferences, arithmetic, and re-casts. This provides a way to get around the many constraints imposed on references by permissions, lifetimes, allocators and bounds-checks.
- Use of functions written in and imported from other languages. This is normally disallowed since most other languages are not as rigorous in verifying the safety of the code.
- Static mutable variable access and mutation. This guard exists to protect against race conditions in multi-threaded code.
Safety is enriched when the programmer and the compiler work together towards that end, each bringing different gifts to bear on the problem. It would not be fair to imply that the compiler's inability to verify safety means that the code is inherently unsafe. By using this "trust" mechanism, compiler-unverifiable code can be isolated and highlighted as such, while still acknowledging that the programmer, who has the ultimate responsibility for safety, may very clearly see what the compiler cannot.