This is a tutorial on the Céu programming language.
We assume some knowledge in C, as most of the basic functionality of Céu derives from it (constants, operators, assignments, pointers, arrays, declarations, etc).
Hello World!
Follows a reactive Hello World! in Céu that prints the infamous message every 250 milliseconds:
loop do
await 250ms;
_printf("Hello World!\n");
end
The await
statement halts the running line of execution until the
referred event occurs (in this case, the elapsing of 250 milliseconds).
In Céu, external C symbols like printf
must be preceded with an
underscore.
The loop
statement in the example repeats its body indefinitely.
==> Timers are integrated with the language!
I/O Events:
Céu also supports events that represent external input/output, allowing programs to interact with the environment:
// external events identifiers begin in uppercase
input int Evt1; // `Evt1' is an input event of integers
output void Evt2; // `Evt2' is an output event (w/o return status)
loop do
int v = await Evt1; // `v' gets the next triggered value of `Evt1'
_printf("Evt1=%d\n", v);
if v == 0 then
break; // escapes the loop if v==0
end
emit Evt2(v); // emits Evt2=v
end
==> The await statement accounts for the reactive nature of Céu.
An external event is either of type input or output, i.e., it is not possible
to emit
an input event, nor to await
an output event.
An input event in Céu represents a correspondent event in the underlying platform. For example, in an Arduino binding for Céu, a change in an input pin might trigger a correspondent input event (e.g. PIN02).
Conversely, an output event causes an effect in the underlying platform.
For example, for an Arduino, an emit
in an output event might change
the state of an output pin (e.g. PIN13).
In higher-level platforms (e.g. operating systems), more abstract events might be available, such as events to handle network packets, or a keyboard. The availability of external events depend on the platform in use.
Parallel Compositions:
If only a single line of execution were allowed, a program could not wait for multiple events at the same time, and Céu would be the most worthless language in the World.
A line of execution is known as a trail, and Céu allows multiple trails to coexist.
The parallel statements of Céu (par/and
, par/or
, par
) splits
the running trail in two:
loop do
par/and do
await 100ms;
_printf("Hello ");
with
await 250ms;
_printf("World!\n");
end
end
The par/and
stands for parallel and, meaning that the trails spawned in
parallel rejoin after both terminate (restarting the loop
, in the
example).
A par/or
rejoins after any spawned trail in parallel terminates, killing
the other.
If we change the par/and
to a par/or
, the second _printf
is
never reached:
loop do
par/or do
await 100ms;
_printf("Hello ");
with
await 250ms;
_printf("World!\n"); // never reached
end
end
We can change the original par/and
example to terminate on the occurrence
of the input event TERM
.
We just need to enclose the whole code with another par/or
that awaits
the termination event in parallel:
input void TERM;
par/or do
loop do // the original loop never terminates
par/and do
await 100ms;
_printf("Hello ");
with
await 250ms;
_printf("World!\n");
end
end
with
await TERM; // but the whole par/or terminates on TERM
end
return 1;
Note that we don't need to change a single line in the original par/and
loop.
==> Parallel compositions in Céu are very powerful!
The par
statement can be used when the trails in parallel never
terminate.
In this case, using par/and
or par/and
would also have the same
effect, but could lead to confusion:
input void Hello;
input void World;
par do -- par/and, par/or would behave the same
loop do
await Hello;
_printf("Hello!\n");
end
with
loop do
await World;
_printf("World!\n");
end
end
Execution Model:
The existence of a parallel statement raises several questions regarding how trails are scheduled during execution. Nonetheless, the concurrency properties of Céu are quite simple and easy to grasp.
==> Céu doesn't require semaphores, locks, or any other synchronization primitive!
The following rules are respected during the execution of programs:
#1 Synchronous execution:
A program execution is synchronous with respect to input events. This means that while trails are reacting to the current input event, no further events are handled.
The period in which trails are reacting to a given input event is named as a reaction chain. A trail only halts (i.e. stops to react) when it awaits again or terminates.
In the following example, suppose the event A
occurs just before B
:
input void A, B;
int v = 0;
par/and do
await A; // first trail
v = v + 1;
with
await B; // second trail
v = v * 2;
end
return v;
The occurrence of A
awakes the first trail that performs the increment on
v
.
If the event B
occurs in the middle of the increment operation, it is
delayed until the running reaction chain terminates.
Hence, there is no possible race condition on accessing v
and the only
possible result for the program is 2 ((0+1)*2).
==> Reactions to input events do not overlap.
But what if a trail executes endlessly and never halts, how further input events could be handled? (see #2)
And what if multiple trails react to the same input event and access the same variables? (see #3, #4)
#2 Bounded execution:
Céu ensures at compile time that a trail never runs forever, and hence, that reaction chains in Céu always run in bounded time.
The compiler detects the only way a trail could run in unbounded time: loops that do not await, the so called tight loops.
In the following examples, the loop bodies all have a path that does not await:
loop do // a tight loop
nothing; // `nothing' is a valid statement :)
end
or
loop do // a tight loop
par/or do
await A;
with
nothing; // this par/or path does not await
end
end
or
loop do // a tight loop
if a == 0 then
await A;
end // the omitted else does not await
end
// calculates the sum from 1..100
int sum = 0;
int i = 1;
loop do // a tight loop
sum = sum + i;
if i == 100 then
break;
else
i = i + 1; // this path does not await or break
end
end
return sum;
==> Céu detects tight loops at compile time!
The last example is actually useful and it would be a shame if it couldn't be written in Céu. (see Asynchronous Execution)
The tight loop analysis is not extended for external C code. It is the responsibility of the programmer to ensure that external functions run in bounded time.
#3 Deterministic execution:
Céu is designed to be deterministic in the sense that a given program should always yield the same outcome for the same timeline (i.e. a sequence of input events) executed multiple times.
However, as trails share variables, it is easy to write non-deterministic programs.
In the following example, the variable v
is accessed concurrently on the
occurrence of A
:
input void A;
int v;
par/and do
await A;
v = 1; // non-deterministic access
with
await A;
v = 2; // non-deterministic access
end
return v; // returns 1 or 2
Céu performs a static analysis to detect non-deterministic access to variables, raising a warning if it is the case.
In the next example, although v
is accessed in both trails on the
occurrence of A
, they cannot happen at the same time:
input void A;
int v;
par/and do
await A;
v = 1; // deterministic access
with
await A;
await A;
v = 2; // deterministic access
end
return v; // returns 2
The static analysis takes into account any combinations of events, timers, loops, parallel statements, etc, as the following examples illustrate:
int v = 0;
par/or do
loop do
await 10ms;
v = v + 1; // non-deterministic access (on 10th iteration)
end
with
await 100ms;
v = v * 2; // non-deterministic access
end
return v; // returns 19 or 20
input void A,B;
int v;
par/and do
par/and
await A;
with
await B;
end
v = 1; // non-deterministic access
with
await A;
await B;
v = 2; // non-deterministic access
end
return v; // returns 1 or 2
==> Céu detects non-deterministic access to variables at compile time!
#4 Atomic execution:
Some applications are inherently non-deterministic and require the deterministic property of Céu to be relaxed. (That's why the static analysis raises a warning instead of an error.)
For these situations, Céu ensures that the trails' segments that access the same variables concurrently execute atomically, i.e., they are never interrupted:
input void A, B, C;
int v = 0;
par/and do
await A;
// atomic begin: trail awakes
v = v + 1; // non-deterministic access
// atomic end: trail halts
await B;
v = v + 1; // deterministic, no need to be atomic
with
await A;
// atomic begin: trail awakes
v = v * 2; // non-deterministic access
// atomic end: trail halts
await C;
v = v * 2; // deterministic, no need to be atomic
end
return v;
==> Céu prevents race conditions on concurrent access to shared variables!
#5 Glitch-free execution:
A glitch is an intermitent runtime state that may cause an undesired effect in the program.
In the following example, both trails in the par/or
terminate at the same
time:
1: int v1=0, v2=0;
2: par/or do
3: v1 = 1; // priority=2
4: with
5: v2 = 2; // priority=2
6: end
7: return v1 + v2; // priority=1 (returns 3)
Once one of the trails (e.g. line 3) terminate, the par/or
composition
could proceed to its join point (line 7).
However, the trail in parallel (e.g. line 5) is also executing, what would make
the par/or
terminate again and re-execute the join point, characterizing
a glitch.
In order to avoid glitches, Céu assigns different priorities to statements at
compile time, so that they execute in the expected order.
In the example, both assignments execute before the return
statement.
==> Céu is glitch-free!
Internal events:
Every variable in Céu is an internal event and vice-versa.
Programs can await
for changes in variables, which are signalled through
the emit
statement:
// internal events (variables) identifiers begin in uppercase
int i = 1;
par do
loop do
_printf("Hello World: %d!\n", i);
await i; // waits for changes
end
with
loop do
await 250ms;
emit i(i+1); // assigns a new value and triggers `i'
end
end
Internal events are always of both input and output types. They are used as the communication mechanism among trails.
The next example holds the constraint that v1
is always v2+1
:
input int Start;
int v1, v2;
par/or do
loop do
await v2;
v1 = v2 + 1;
end
with
loop do
await v1;
v2 = v1 - 1;
end
with
// tests the constraint
await Start;
emit v1(2); // use `emit' instead of `='
_printf("v1=%2d v2=%2d\n", v1, v2);
emit v2(12);
_printf("v1=%2d v2=%2d\n", v1, v2);
emit v1(0);
_printf("v1=%2d v2=%2d\n", v1, v2);
end
==> Variables in Céu can be reactive!
Stacked Execution
Although internal and external events use the same reactivity primitives
(await
and emit
), they behave quite differently.
For instance, when an external output event is emitted, it has no effect on the
own program, only on the underlying platform.
For internal events, this is not the case: a running trail that emits an internal event may cause another trail to awake.
In Céu, when a trail emits an internal event, it immediately halts, resuming only after the reaction to it terminates:
input int Start;
int e = 0;
par do
loop do
await e;
e = e + 1;
end
with
await Start;
// 1st trail is awaiting `e'
emit e(); // halts and resumes after `e=e+1'
// 1st trail is awaiting `e' again
emit e(); // halts and resumes after `e=e+1'
_assert(e == 2);
return e;
end
The way internal events execute is analogous to conventional call/return routines. When a routine is invoked, the statement following it only executes after the routine returns. Also, just like routines, internal events can nest to a deep level of emits.
==> Internal events execute similar to routines!
Asynchronous Execution:
The sum example previously shown contains a tight loop and cannot be written that way.
The asynchronous blocks (asyncs) of Céu are the way to perform time consuming computations:
int ret = async do
// calculates the sum from 1..100
int sum = 0;
int i = 1;
loop do
sum = sum + i;
if i == 100 then
break;
else
i = i + 1; // this path does not await or break
end
end
return sum;
end;
Code that runs in async
blocks is suspended whenever there is a pending
input event to the synchronous side.
This way, you never know if/when an async
executes.
Asyncs cannot await, cannot use parallel compositions, and cannot nest. Nonetheless, they can still be used in conjunction with the synchronous side of the application:
int ret;
par/or do
ret = async do
// calculates the sum from 1..100
// (copy of the previous sum)
end;
with
await 1s; // watchdog to kill the async if it takes too long
ret = 0;
end
return ret;
==> Asynchronous blocks allow programs to perform long computations!
Simulation:
Asyncs are allowed to trigger input events and the passage of time towards the synchronous side of a program, providing a way to test programs in the own language:
input int A;
// tests a program with a simulation in parallel
par/or do
// original program
int v = await A;
loop do
await 10ms;
_printf("v = %d\n", v);
v = v + 1;
end
with
// input simulation
async do
emit A(0); // initial value for `v'
emit 1s35ms; // the loop executes 103 times
end
end
When the program starts, the par/or
spawns the original program with the
input simulation.
As synchronous code has higher priority, the program immediately awaits the
event A
.
Then, the async
emits the event A
, awaking the synchronous code,
which enters the loop, prints the message, and awaits 10ms.
Then, the async
resumes and emits 1s35ms, what makes the synchronous
side to resume, increment v
, restart the loop, print the message, and
await 10ms again.
However, 1s25ms remains from the previous emit
, what makes the
synchronous side to resume again, and again (the loop iterates and prints the
message exactly 103 times).
==> Céu supports the simulation of programs in the own language!
C Definitions:
Céu has no support for functions or new types, which can be defined inside C blocks:
C do
int soma (int a, int b) {
return a+b;
}
typedef struct {
int a;
int b;
} mystruct;
end
_mystruct s;
int v = _soma(s.a, s.b);
As previously stated, C symbols must be preceded with an underscore when used in Céu programs.
The side effects of output events must also be defined in C. The complete version for the I/O Events example in the beginning of the tutorial is as follows:
// external events identifiers begin in uppercase
input int Evt1; // `Evt1' is an input event of integers
output void Evt2; // `Evt2' is an output event (w/o return status)
loop do
int v = await Evt1; // `v' gets the next triggered value of `Evt1'
_printf("Evt1=%d\n", v);
if v == 0 then
break; // escapes the loop if v==0
end
emit Evt2(v); // emits Evt2=v
end
C do
// defines the behavior for `Evt2'
void Evt2 (int v) {
printf("Evt2 emitted: %d\n", v);
}
end
C blocks can only be used for global definitions.
==> Céu integrates well with C!
Platforms:
The Céu compiler converts a Céu source file (e.g. my_prog.ceu) into a single equivalent C source file (e.g. my_prog.c):
./ceu --output my_prog.c my_prog.ceu
The generated file contains not only the actual code, but also functions and
hooks to connect with your target platform.
The platform file containing the equivalent of the function main
has to
include the generated file and call the predefined functions to initialize Céu
(ceu_go_init
), feed it with events (ceu_go_event
), advance time
(ceu_go_time
), and execute asynchronous code (ceu_go_async
).
In other words, a binding C file for each platform is used to command the
execution of the Céu generated file.
For the examples in this tutorial, which run on the web-server, we use the
following main.c
:
#include "my_prog.c"
int main (void)
{
int ret = 0;
int async_cnt;
if (ceu_go_init(&ret, 0))
return ret;
for (;;) {
if (ceu_go_async(&ret,&async_cnt))
return ret;
if (async_cnt == 0)
break;
}
return ret;
}
With this binding, for each example, we concatenate an async block in parallel that fires events and time towards the synchronous side, as shown in the simulation section.
For a real world binding, consult this wiki for specific instructions regarding the Arduino platform.
Acknowledgments:
Developed by Francisco Sant'Anna at PUC-Rio.
Did you like Céu? Would you like to use it? Please, let me know, I can help you!