Statements
In its core, Blech is an imperative language. Blech strictly separates between statements that govern the flow of control and expressions that evaluate to a value. Both, a statement and an expression may have side-effects.
Activities and functions
Control flow is organised with two kinds of procedures: activities and functions. Functions are used like in any other imperative language. In Blech they have two parameter lists to support causality analysis.
See the section on subprogram declarations for more details.
Functions must terminate within a reaction. Therefore they are said to be instantaneous.
Activities are called similarly to functions but their execution continues until it reaches a pause to end the current reaction. In the next reaction they resume from the location they have paused and continue execution until the next pause. Each reaction of an activity must end after finitely many computation steps. However the activity itself may perform an unbounded number of reactions and never terminate. It may also terminate after a finite number of reactions. In any case, activities must not be instantaneous—they need to pause at least once before termination. The values of local variables in activities are stored from one reaction to the next.
Activities may call other activities or functions. Functions however can only call other functions.
Synchronous statements
Here we discuss statements that are specific to the synchronous paradigm of Blech. These statements control the reactive behaviour of the program and hence may only appear inside activities, not in functions.
Await
The aforementioned “pause” is denoted as follows.
Pause ::= "await" Condition
The await
statement marks the end of a reaction.
The control flow stops at this statement and the program “waits” for the next reaction to start.
Once an activity resumes its execution at an await
statement the condition is checked.
If it is true, the control flow proceeds to the next statement.
Otherwise, the reaction ends immediately and the control flow remains at the same location.
Example: Simple reactive control flow
|
|
Initially, MyAct
is entered and the control flow proceeds to line 3 and stops (regardless of the inputs).
In the next tick, MyAct
resumes its execution at the await
statement in line 3.
It checks the boolean input variable in1
.
If it is false, the reaction ends immediately, the control flow does not advance.
Otherwise, if in1
is true, the calculation is carried out in line 4.
This updates out1
to a new value.
Finally, if input in2
is indeed less than 0 the activity terminates its execution.
Otherwise the control flow loops around from line 5 back to line 2 and finally the reaction ends again in line 3.
Example: Proceed with the next tick
For example in time triggered systems we often just want to await the next period and proceed. This is simply expressed by
await true
Run
An activity call is given by the following grammar.
ActivityCall ::= "run" [Receiver "="] Identifier RhsArgList [LhsArgList]
Receiver ::= (Wildcard | Identifier | ("var" | "let") Identifier [":" Type])
RhsArgList ::= "()" | "(" RhsExpr ("," RhsExpr)* ")"
LhsArgList ::= "()" | "(" LhsExpr ("," LhsExpr)* ")"
The run
keyword indicates an activity call.
Arguments must be provided that match the callee’s declaration in number and type.
If the callee does not declare any outputs the second pair of parentheses may be dropped for readability.
Input arguments must evaluate to a value that matches the declared type.
Output arguments must evaluate to a memory location that the callee can read from and write to.
If the callee is an activity that eventually terminates and declares a return value, this return value must be either received into some variable or ignored using a wildcard. The receiving variable is either a mutable variable declared earlier or can be declared inside the run
statement. In this case it can also be declared as a read-only variable using let
. The receiver may be used in the code sequentially after the run
statement.
When control flow reaches a run
statement the sub-activity is immediately called and the control flow is handed over to the callee.
It remains within the callee for as many reactions as it runs (but at least one reaction).
In every reaction inputs are passed though to the callee and its outputs are propagated outside.
Should the callee terminate, the control flow returns to the caller and proceeds with the next statement.
Example:
// declaration
activity A (a: [8]int32, b: int32) (c: int32) returns bool
/* some code */
...
end
/* ... inside another activity ... */
var result: bool
var array: [8]int32 = {1, 2, 3, 4, 5, 6, 7, 8}
var output: int32
// usage
run result = A(array, 7)(output)
// alternative: declare receiver within "run"
run let result2 = A(array, 7)(output)
Cobegin
Concurrent composition is expressed with the cobegin
statement.
ForkJoin ::= "cobegin" ["weak"] StmtBlock ("with" ["weak"] StmtBlock)+ "end"
Using cobegin
it is possible to compose arbitrary pieces of code.
The goal is not to parallelise to gain execution speed.
Rather this the language construct to express that two (or more) functionalities should be computed within the same reaction.
As such concurrency is rather a modelling concept in Blech.
The code generator will actually sequentialise the code in a causally correct order.
Note
In the future there will be the possibility to specify truly parallel executions in Blech which can be carried out independently.Example: Concurrent composition
|
|
Assume the activities A
and B
have already been implemented.
In lines 5 – 9 they are composed concurrently.
This means the control flow of P
is forked into two control flow points.
One resides in A
(line 6) and one in B
(line 8).
With every tick both, A
and B
, will perform one reaction.
When both subprograms terminate, P
regains control in line 9 and, in this example, terminates too.
Of course, more than two branches can be combined using more with
branches.
Note that the reaction of B
will be carried out before the reaction of A
due to causality:
First the unique value of x
needs to be set by writer B
before the reader A
may use it.
The cobegin
statement is a composite statement like a repeat
loop or an if
statement.
Control flow can remain within the cobegin
branches for multiple reactions.
However in general the various branches can perform a different number of reactions, possibly infinitely many.
We therefore need to control the termination of cobegin
as a whole using the weak
qualifiers.
Formally, a cobegin
statement terminates in the reaction in which all strong branches have terminated.
If all branches are weak, the cobegin
statement terminates in the reaction in which some branch terminates.
In the above example no branch is weak
.
We say all branches are “strong”.
This means the cobegin
statement terminates when all of its branches have terminated.
Branches that finish earlier (have fewer reaction to do) will simply do nothing until the last strong branch has terminated.
The following two examples illustrate the usage of the weak
qualifier.
Example: Weak and strong branches
/* ... inside some activity ... */
var res: nat8
cobegin weak
// non-terminating branch
repeat
await true
out1 = (out1 + 1) % 100
end
with
// terminates once a key is pressed
res = run ReadKeyStroke()
end
The first branch contains an infinite loop in the sense that every iteration ends in a pause but the number of reactions is unbounded.
The loop is placed into a weak branch that allows to terminate the loop at the end of some reaction.
The second branch contains an activity call that is expected to return some value eventually.
In the reaction in which the result is returned the weak branch will be aborted (after it has done one iteration) and the control flow continues with the next statement following the cobegin
.
Example: All branches weak
cobegin weak
await isButtonPressed
with weak
await hasReceivedSignal
end
In this example there are no strong branches.
The first branch to terminate will abort all others.
In this example it means as soon as isButtonPressed
or hasReceivedSignal
is true (or both are true!) the cobegin
statement terminates and control flow continues with the next statement.
Prev
prev
is not a statement but a special operator which is most useful in the context of a cobegin
block.
The introductory chapter explained causality
.
In short, this means two concurrent branches may not write the same shared memory and furthermore cyclical read-write dependencies are prohibited as well.
Sometimes however we need to express a quasi-cycle wherein one branch starts off with a value that has already been computed in the previous reaction. This is conveniently expressed using the prev
operator.
Example: Previous values
cobegin
run A (prev x)(y)
with
run B (y)(x)
end
Here, in every reaction, the previous value of x
, i.e. the result of the previous reaction, is given to A
which performs a step and produces a new value for y
. This is then used by B
to produce a new current value of x
.
prev
can only be used where we expect to read a value. It cannot be used on a left-hand-side of an assignment or in an output argument position. prev
may only be applied to values, identified by a name. It cannot be used on arbitrary expressions.
When used on memory of complex data types, prev
binds to the outermost part. For a structure s
the expression prev s.x
is to be read as (prev s).x
.
Abort and reset
There are two kinds of synchronous preemptions built into Blech.
Preemption ::= "when" Condition ("abort" | "reset") StmtBlock "end"
When control flow enters a preemption the statements in its body, denoted by StmtBlock
in the grammar, are executed until the reaction ends in an await
or run
statement.
Subsequently, when a reaction resumes execution inside the body, first the when
condition is checked.
If it is false, the execution starts as usual.
Otherwise the body is preempted before any statement is executed or expression is evaluated inside the body.
There are two variants of preemptions.
abort
means that control flow jumps to the end
of the preemption statement.
reset
means that control flow restarts at the beginning of StmtBlock
.
Note that
when Condition reset P end
is syntactic sugar for
var hasTerminated = false
repeat
when Condition abort
P
hasTerminated = true
end
until hasTerminated end
where hasTerminated
is a fresh boolean variable.
It is, of course, possible that the body is left instantaneously within one reaction. In this case the preemption is irrelevant for the flow of control.
Important
The preemption condition is not checked when the control flow enters the body. Thewhen
condition is only checked when control flow resumes from within the body.
Wrap the preemption inside an if
statement in case you want to check the condition before entering the body.
Example: Abort and Reset
|
|
The statement in line 4 says that when
a reaction starts in the block lines 5 – 9, it is checked whether in1
is true and in that case the control flow skips to line 10.
Thus when control flow reaches line 4 it will immediately proceed to line 5, set out1
accordingly and finish this reaction in line 6 (regardless of the value of in1
).
The next reaction starts by checking the abort condition in1
.
If it is true we skip the rest of the block and proceed to line 10.
Otherwise, we check the condition of the await
statement which here is vacuously true and the reaction proceeds to line 7 and finishes in line 8.
The same reasoning applies in line 8: the execution is possibly aborted before setting out1
to 3.
In any case, the block is left in line 10.
The abort
statement is useful whenever we want to skip over a sequence of reactions when we detect some issue at the beginning of a reaction.
Sometimes instead of skipping ahead we would like to restart a sequence of reactions.
For this we may use the reset
statement.
activity MyAct (in1: bool) (out1: uint8)
// do something ...
when in1 reset // reset instead of abort
out1 = 1
await true
out1 = 2
await true
out1 = 3
end
// do something else ...
end
It behaves just like the abort
statement except it jumps to line 4 if in1
is true.
Imperative control flow
All of the following statements are known from mainstream imperative programming languages. They may be used inside both, activities and functions.
Assignment
Assignment ::= LhsExpr "=" RhsExpr
Do block
DoBlock ::= "do" StmtBlock "end"
The do
block may be used to define local scopes.
Example: Local scopes
function f ()
do
var x: int8 = 5
end
// x is out of scope here
end
If
IfStmt ::= "if" Condition "then" StmtBlock ["elseif" Condition "then" StmtBlock]* ["else" StmtBlock] "end"
While
WhileLoop ::= "while" Condition "repeat" StmtBlock "end"
When using a loop in an activity, there must be some pause on every control flow path through the loop body.
This is not necessary for loops inside functions.
Note that using while
loops in activities may lead to unexpected error messages because we deliberately do not evaluate the condition at compile time even if it is trivially true or false.
For example:
activity Toggle()(out:bool)
while true repeat
out = not out
await true
end
end
This program will not compile because the compiler thinks there is a control flow path that does not enter the loop and immediately terminates the activity.
Since activities must not be instantaneous this program is rejected.
Use repeat
loops instead to avoid this spurious control flow paths that possibly skip the loop.
Repeat
RepeatLoop ::= "repeat" StmtBlock ["until" Condition] "end"
repeat
loops guarantee that control flow does enter the loop body at least once.
Endless loops (without the until
condition) may only be used in activities.
Again, when using a loop in an activity, there must be some pause on every control flow path through the loop body.
This is not necessary for loops inside functions.
Return
ReturnStmt ::= "return" [RhsExpr] | "return" ActivityCall
Void activities and functions can use return
without an expression to terminate at some point before control flow reaches the last statement.
Example: Return from void function
function setSpeed (velocity: float32) ()
if velocity < 0 then
return
end
if not isMotorReady() then
return
end
halSetSpeed(velocity) // call device driver
end
This simple example assumes we implement a wrapper for calling a hardware device driver.
In order to avoid an if-then-else cascade setSpeed
tests the prerequisites individually and returns in case some of them is not met.
Activities and functions that declare a return type must return a value of this type on every control flow path that reaches the end of the program body.
Activities may only return from their main thread.
In other words return
must not occur inside a branch of a cobegin
statement.
This is a design decision which avoids cases in which multiple branches could return a value and it is not clear which one “wins” the race. Furthermore, even if only one branch could return, it still would not be clear whether concurrent branches will execute their reactions entirely or not. For the sake of a clear and easy to understand semantics the above restriction is enforced.
Mind the difference between activity return values and activity output values. Outputs are set in every reaction of the activity. A return value is returned precisely once in the reaction that terminates the activity.
Example: Return from activity
activity A (in: int32) (out: int32) returns nat8
var retcode: nat8
var x: int32
cobegin weak
run B(in)(x)
with
retcode = run C(x)(out)
end
return retcode
end
In every reaction in
is propagated to B
and out
is propagated from C
to the caller.
Only when C
terminates the variable retcode
is updated, the cobegin
statement is terminated and the retcode
is returned to the caller.
Activities that simply terminate and pass on the value of their callee may use the syntactic sugar
return run A()
instead of the more verbose
run let foo = A()
return foo
Function call
FunctionCallStmt ::= Identifier RhsArgList [LhsArgList]
Blech distinguishes between a function call statement and a function call expression. On the statement level only void functions may be called.
In summary, a block of statements in Blech is given by the following grammar.
StmtBlock ::=
Pause
| ActivityCall
| ForkJoin
| Preemption
| Assignment
| DoBlock
| IfStmt
| WhileLoop
| RepeatLoop
| ReturnStmt
| FunctionCallStmt
| StmtBlock ";"* StmtBlock
Depending on whether statements are used in an activity or a function their use may be restricted or prohibited (see above). The optional semicolons in the grammar indicate that it is possible to separate statements in a sequence using semicolons. We suggest to avoid writing any semicolons unless two (or more) statements are written in one line.
Semicolons in Blech
function f()
var x: int8 = 0; // do not clutter your code with superfluous ";"
var y: int8 = 1 // preferred semicolon free style
x = 7; y = y + x // ok, but usually it is better to write two lines
x = 7 y = y + x // compiles but is hardly readable, do not do that
end