A Tutorial of Lustre: Nicolas HALBWACHS, Pascal RAYMOND Oct.2002, Rev. Aug. 2007
A Tutorial of Lustre: Nicolas HALBWACHS, Pascal RAYMOND Oct.2002, Rev. Aug. 2007
Contents
1 Basic language 2
1.1 Simple control devices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2 Numerical examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3 Tuples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2 Clocks 10
Bibliography 17
This document is an introduction to the language Lustre V4 and its associated tools. We will
not give a systematic presentation of the language, but a complete bibliography is added. The basic
references are [7, 11]. The most recent features (arrays, recursive nodes) are described in [31].
1
EDGE
X Y
Figure 1: A Node
1 Basic language
A Lustre program or subprogram is called a node. Lustre is a functional language operating on
streams. For the moment, let us consider that a stream is a finite or infinite sequence of values.
All the values of a stream are of the same type, which is called the type of the stream. A program
has a cyclic behavior. At the nth execution cycle of the program, all the involved streams take their
nth value. A node defines one or several output parameters as functions of one or several input
parameters. All these parameters are streams.
This equation defines “Y” (its left-hand side) to be always equal to the right-hand side expression
“X and not pre(X)”. This expression involves the input parameter X and three operators:
• “and” and “not” are usual Boolean operators, extended to operate pointwise on streams: if
A = (a1 , a2 , . . . , an , . . .) and B = (b1 , b2 , . . . , bn , . . .) are two Boolean streams, then A and B
is the Boolean stream (a1 ∧ b1 , a2 ∧ b2 , . . . , an ∧ bn , . . .). Most usual operators are available
in that way, and are called “data-operators”. Here is the list of built-in data operators in
Lustre-V41 :
2
Figure 2: Simulating a node
• The “pre” (for “previous”) operator allows to refer at cycle n to the value of a stream at cycle
n − 1: if A = (a1 , a2 , . . . , an , . . .) is a stream, pre(A) is the stream (nil, a1 , a2 , . . . , an−1 , . . .).
Its first value is the undefined value nil, and for any n > 1, its nth value is the (n − 1)th value
of A.
• The “->” (followed by) operator allows to initialize streams. If A = (a1 , a2 , . . . , an , . . .)
and B = (b1 , b2 , . . . , bn , . . .) are two streams of the same type, then “A->B” is the stream
(a1 , b2 , . . . , bn , . . .), equal to A at the first instant, and then forever equal to B. In particular,
this operator allows to mask the “nil” value introduced by the pre operator.
As a consequence, if X = (x1 , x2 , . . . , xn , . . .) the expression “X and not pre(X)” represents the
stream (nil, x2 ∧ ¬x1 , . . . , xn ∧ ¬xn−1 , . . .). In order to avoid the “nil” value, let us use the ->
operator, and the built-in constant false 2 .
The complete definition of the node EDGE is the following:
node EDGE (X: bool) returns (Y: bool);
let
Y = false -> X and not pre(X);
tel
3
In this example, the mode is not very important, since there is only one input. Try, for instance,
the auto-step mode:
We get a file EDGE.oc which contains the object code written in the Esterel-Lustre common
format oc [22]. We can simulate this program using the Lux simulator, by typing:
lux EDGE.oc
The oc code is translated into an instrumented program EDGE.c. A standard main loop program is
also generated in a file EDGE loop.c. Then the two files are compiled and linked into an executable
program EDGE. Calling EDGE we get
asking for a first value of X, of type bool. We type “1”, and get
The first value of Y is false, and a new value is wanted for X. We can then continue the simulation,
and terminate it by “^C”.
Let us have a look at the C code, in the file EDGE.c. The file contains some declarations, and
the procedure EDGE step, shown below, which implements the generated automaton. The procedure
selects the code to be executed according to the value of the context variable “ctx->current state”,
which is initialized to 0.
4
0
¬x/¬y
x/¬y
¬x/¬y
x/¬y 1 2 ¬x/¬y
x/y
break;
case 1:
ctx->_V2 = _false;
EDGE_O_Y(ctx->client_data, ctx->_V2);
if(ctx->_V1){
ctx->current_state = 1; break;
} else {
ctx->current_state = 2; break;
}
break;
case 2:
if(ctx->_V1){
ctx->_V2 = _true;
EDGE_O_Y(ctx->client_data, ctx->_V2);
ctx->current_state = 1; break;
} else {
ctx->_V2 = _false;
EDGE_O_Y(ctx->client_data, ctx->_V2);
ctx->current_state = 2; break;
}
break;
} /* END SWITCH */
EDGE_reset_input(ctx);
}
5
ocmin EDGE.oc -v
That means that the automaton was not minimal, and a minimal one, with only two states,
was written in the file EDGE min.oc.
• The Lustre compiler can directly produce a minimal automaton using the -demand option3 :
We get:
6
initial/level 0 ¬initial/¬level
reset/¬level
1 2
¬reset/level ¬set/¬level
\cty{set/level}
7
1.2 Numerical examples
1.2.1 The counter node
It is very easy in Lustre to write a recursive sequence. For instance the definition C = 0 -> pre C
+ 1; defines the sequence of natural. Let us complicate this definition to build a integer sequence,
whose value is, at each instant, the number of “true” occurences in a boolean flow X:
C = 0 -> if X then (pre C + 1) else (pre C);
This definition does not meet exactly the specification, since it ignores the initial value of X. A well
initialized counter of X occurences is for instance:
PC = 0 -> pre C;
C = if X then (PC + 1) else PC;
Let us complicate this example to obtain a general counter with additionnal inputs:
• an integer init which is the initial value of the counter.
• a boolean reset which sets the counter to the value of init, whatever is the value of X.
The complete definition of the counter is:
node COUNTER(init, incr : int; X, reset : bool) returns (C : int);
var PC : int;
let
PC = init -> pre C;
C = if reset then init
else if X then (PC + incr)
else PC;
tel
This node can be used to define the sequence of odd integers:
odds = COUNTER(1, 2, true, true->false);
Or the integers modulo 10:
reset = true -> pre mod10 = 9;
mod10 = COUNTER(0, 1, true, reset);
8
init 0
STEP - 1 2 1 1 2
F 1 2 0 -1 1 2
Y 0 1.5 3.5 3 3 6
9
1.3 Tuples
1.3.1 Nodes with several outputs
The node sincos 1.2.3 does not work very well, but it is interesting, because it returns more than
one output. In order to call such nodes, Lustre syntax allows to write tuples definition. Let s, c
and omega be tree real variables, (s, c) = sincos(omega) is a correct Lustre equation defining
s and c to be respectively the first and the second result of the call. The following node compute
how the node sincos (badly) meets the Pythagore theorem:
• a “if ... then ... else” whose two last operands are tuples. The “if” operator is the
only built-in operator which is polymorphic.
Tuples can be used to “factorise” the definitions, like in the following node minmax:
2 Clocks
Let us consider the following control device: it receives a signal “set”, and returns a Boolean
“level” that must be true during “delay” cycles after each reception of “set”. The program is
quite simple:
10
Now, suppose we want the “level” to be high during “delay” seconds, instead of “delay” cycles.
The second will be provided as a Boolean input “second”, true whenever a second elapses. Of
course, we can write a new program which freezes the counter whenever the “second” is not there:
We can also reuse our node “STABLE”, calling it at a suitable clock, by filtering its input parameters.
It consists of changing the execution cycle of the node, activating it only at some cycles of the calling
program. For the delay be counted in seconds, the node “STABLE” must be activated only when either
a “set” signal or a “second” signal occurs. Moreover, it must be activated at the initial instant, for
initialization purposes. So the activation clock is:
Now a call “STABLE((set,delay) when ck)” will feed an instance of “STABLE” with rarefied
inputs, as shown by the following table:
According to the data-flow philosophy of the language, this instance of “STABLE” will have a cycle
only when getting input values, i.e., when ck is true. As a consequence, the inside counter will have
the desired behavior, but the output will also be delivered at this rarefied rate. In order to use the
result, we have first to project it onto the clock of the calling program. The resulting node is
11
(set,delay) (tt,2) (ff,2) (ff,2) (ff,2) (ff,2) (ff,2) (ff,2) (tt,2) (ff,2)
(second) ff ff tt ff tt ff ff ff tt
ck tt ff tt ff tt ff ff tt tt
(set,delay)
(tt,2) (ff,2) (ff,2) (tt,2) (ff,2)
when ck
STABLE((set,delay)
tt tt ff tt tt
when ck)
current(STABLE
(set,delay) tt tt tt tt ff ff ff tt tt
when ck))
where the 1-bit adder ADD takes as input two bits and an input carry, and returns their sum and an
output carry:
12
a0 b0 a1 b1 a2 b2 a3 b3
c0 c1 c2 c3
0 ADD1 ADD1 ADD1 ADD1
s0 s1 s2 s3 carry
A B
false
ADD1 ADD1
S carry
Instead, we can consider A and B as arrays, made of 4 Booleans. “bool^4” denotes the type of
“arrays of 4 Booleans”, indexed from 0 to 3 (the “^” operator here refers to Cartesian power). The
adder node becomes (see Fig. 7):
13
The first equation defines the first components of S and C using the standard indexation notation
(notice that arrays can only be indexed by compile-time expressions). The second equation is less
standard, and makes use of slicing and polymorphism:
• the notation “S[1..3]” refers to the “slice” of the array S, made of elements 1 to 3 of S, i.e.,
the array X of type bool^3 such that
• From its declaration, the node ADD1 takes three Booleans as input parameters, and returns 2
Booleans. Here, it is called with three Boolean arrays (of the same size) as input parameters,
and returns 2 Boolean arrays (of the same size as the input arrays), obtained by applying ADD1
componentwise to the input arrays. Such a polymorphic extension is available for any operator
of the language.
So, the equation “(S[1..3],C[1..3]) = ADD1(A[1..3],B[1..3],C[0..2])” stands for
(S[1],C[1]) = ADD1(A[1],B[1],C[0]);
(S[2],C[2]) = ADD1(A[2],B[2],C[1]);
(S[3],C[3]) = ADD1(A[3],B[3],C[2]);
The expansion of this node is the first task of the compiler. It consists, more or less, in translating
ADD4 into FIRST ADD4, by replacing any array element by a variable defined by its own equation.
Now, we can also define a general binary adder, taking the size of the arrays as a parameter:
node ADD (const n: int; A,B: bool^n)
returns (S: bool^n; carry: bool);
var C: bool^n;
let
(S[0],C[0]) = ADD1(A[0],B[0],false);
(S[1..n-1],C[1..n-1]) = ADD1(A[1..n-1],B[1..n-1],C[0..n-2]);
carry = C[n-1];
tel
Such a node cannot be compiled alone. As a matter of fact, the compiler needs an actual value to be
given to the parameter n, in order to be able to expand the program. A main node must be written,
for instance:
node MAIN_ADD (A,B: bool^4) returns (S: bool^4);
var carry: bool;
let
(S, carry) = ADD(4,A,B);
tel
or, better, defining the size as a constant:
const size = 4;
node MAIN_ADD (A,B: bool^size) returns (S: bool^size);
var carry: bool;
let
(S, carry) = ADD(size,A,B);
tel
14
3.3 The exclusive node
Let us show another example making use of arrays: In §?? we will need an extension of the “#”
(exclusion) operator to arrays, i.e., an operator taking a Boolean array X as input, and returning
“true” if and only if at most one of X’s element is true. We use two auxiliary Boolean arrays: An
array EX whose ith element is true if there is at most one true element in X[0..i], and an array OR
to compute the cumulative disjunction of X’s elements:
In other words:
EX[i+1] = EX[i] ∧ ¬(OR[i] ∧ X[i+1]) with EX[0] = true
OR[i+1] = OR[i] ∨ X[i+1] with OR[0] = X[0]
One can write the corresponding node as follows:
• The constructor “[.]”: If X:τ ^m and Y:τ ^n are two arrays, “X|Y” is their concatenation, of
type τ ^(m+n).
• The concatenation “|”: If E0, E1, ..., En are n expressions of the same type τ , then
“[E0, E1, ...,En]” is the array of type τ ^(n+1) whose ith element is Ei (i = 0 . . . n).
In the equation defining “EX”, the Boolean “true” has been converted into the array of one
Boolean “[true]” to be given to the concatenation operator.
15
The expression “false^(d)” denotes an array of size d, all the elements of which are false. It is the
initial value of the slice A[1..d]. Notice the polymorphic extensions of the operators -> and pre.
To compile this program, we have again to call it from a main node:
However, compiling such a program into an automaton is not a good idea (Try it): The call
“DELAY(10,A)” creates 10 Boolean memories (instances of a pre operator) wich will involve 210
states in the automaton. Instead, one can call the compiler with the option “-0”,
which produces a single-loop code: The resulting automaton has only one state and one (complicated)
transition.
16
A
OR
OR
(a) (b)
set/ok
¬set ¬reset
/ok /ok /ok
reset/ok
(a) (b)
References
[1] J-L. Bergerand. Lustre: un langage déclaratif pour le temps réel. Thesis, Institut National
Polytechnique de Grenoble, 1986.
[2] J-L. Bergerand, P. Caspi, N. Halbwachs, D. Pilaud, and E. Pilaud. Outline of a real-time
data-flow language. In 1985 Real-Time Symposium, San Diego, December 1985.
[3] J-L. Bergerand, P. Caspi, N. Halbwachs, and J. Plaice. Automatic control systems programming
using a real-time declarative language. In IFAC/IFIP Symp. ’SOCOCO 86, Graz, May 1986.
[4] B. Berkane. Vérification des systèmes matériels numériques séquentiels synchrones : Applica-
tion du langage Lustre et de l’outil de vérification Lesar. Thesis, Institut Polytechnique de
Grenoble, October 1992.
[5] C. Buors. Sémantique opérationnelle du langage Lustre. DEA Report, University of Grenoble,
June 1986.
17
[6] P. Caspi. Clocks in dataflow languages. Theoretical Computer Science, 94:125–140, 1992.
[7] P. Caspi, D. Pilaud, N. Halbwachs, and J. Plaice. Lustre: a declarative language for program-
ming synchronous systems. In 14th ACM Symposium on Principles of Programming Languages,
POPL’87, Munchen, January 1987.
[8] A. Girault and P. Caspi. An algorithm for distributing a finite transition system on a
shared/distributed memory system. In PARLE’92, Paris, July 1992.
[9] A-C. Glory. Vérification de propriétés de programmes flots de données synchrones. Thesis,
Université Joseph Fourier, Grenoble, France, December 1989.
[11] N. Halbwachs, P. Caspi, P. Raymond, and D. Pilaud. The synchronous dataflow programming
language Lustre. Proceedings of the IEEE, 79(9):1305–1320, September 1991.
[12] N. Halbwachs and F. Lagnier. Sémantique statique du langage lustre - version 3. Technical
Report SPECTRE L15, IMAG, Grenoble, February 1991.
[13] N. Halbwachs, F. Lagnier, and C. Ratel. An experience in proving regular networks of processes
by modular model checking. Acta Informatica, 29(6/7):523–543, 1992.
[14] N. Halbwachs, F. Lagnier, and C. Ratel. Programming and verifying real-time systems by means
of the synchronous data-flow programming language Lustre. IEEE Transactions on Software
Engineering, Special Issue on the Specification and Analysis of Real-Time Systems, September
1992.
[15] N. Halbwachs, A. Lonchampt, and D. Pilaud. Describing and designing circuits by means of a
synchronous declarative language. In IFIP Working Conference “From HDL Descriptions To
Guaranteed Correct Circuit Designs”, Grenoble, September 1986.
[16] N. Halbwachs and D. Pilaud. Use of a real-time declarative language for systolic array design
and simulation. In International Workshop on Systolic Arrays, Oxford, July 1986.
[17] N. Halbwachs, D. Pilaud, F. Ouabdesselam, and A.C. Glory. Specifying, programming and
verifying real-time systems, using a synchronous declarative language. In Workshop on Au-
tomatic Verification Methods for Finite State Systems, Grenoble. LNCS 407, Springer Verlag,
June 1989.
[18] N. Halbwachs, P. Raymond, and C. Ratel. Generating efficient code from data-flow programs.
In Third International Symposium on Programming Language Implementation and Logic Pro-
gramming, Passau (Germany), August 1991. LNCS 528, Springer Verlag.
[19] D. Pilaud and N. Halbwachs. From a synchronous declarative language to a temporal logic
dealing with multiform time. In M. Joseph, editor, Symposium on Formal Techniques in Real-
Time and Fault-Tolerant Systems, Warwick, September 1988. LNCS 331, Springer Verlag.
18
[21] J. A. Plaice and N. Halbwachs. Lustre-v2 user’s guide and reference manual. Technical Report
SPECTRE L2, IMAG, Grenoble, October 1987.
[22] J. A. Plaice and J-B. Saint. The Lustre-Esterel portable format. Unpublished report,
INRIA, Sophia Antipolis, 1987.
[23] C. Ratel. Etude de la conformité d’un programme Lustre et de ses spécifications en logique
temporelle arborescente. DEA Report, Institut National Polytechnique de Grenoble, June 1988.
[24] C. Ratel. Définition et réalisation d’un outil de vérification formelle de programmes Lustre: Le
système Lesar. Thesis, Université Joseph Fourier, Grenoble, June 1992.
[25] C. Ratel, N. Halbwachs, and P. Raymond. Programming and verifying critical systems by
means of the synchronous data-flow programming language Lustre. In ACM-SIGSOFT’91
Conference on Software for Critical Systems, New Orleans, December 1991.
[26] P. Raymond. Compilation séparée de programmes Lustre. Technical Report SPECTRE L5,
IMAG, Grenoble, June 1988.
[27] P. Raymond. Compilation efficace d’un langage déclaratif synchrone : Le générateur de code
Lustre-v3. Thesis, Institut National Polytechnique de Grenoble, November 1991.
[28] F. Rocheteau. Programmation d’un circuit massivement parallèle à l’aide d’un langage déclaratif
synchrone. Technical Report SPECTRE L10, IMAG, Grenoble, June 1989.
[32] G. Thuau and B. Berkane. Using the language Lustre for sequential circuit verification. In
International Workshop on Designing Correct Circuits, Lingby (Denmark), January 1992.
[33] G. Thuau and D. Pilaud. Using the declarative language Lustre for circuit verification. In
Workshop on Designing Correct Circuits, Oxford, September 1990.
19