Imperative Language¶
Coco includes a conventional imperative programming language, which is used primarily to describe how state machines
respond to events. The imperative programming language is expression orientated, meaning that almost all language
constructs are expressions and thus have a value. This is in contrast to C-like languages in which, for instance,
if
is a statement, not an expression.
Types¶
-
type
Type
¶
Type Reference¶
A type reference refers to an existing non-generic type declaration or constant.
-
type
Type Reference
¶
Consider the following example of type reference representations:
In the variable declaration of size
, Int
is an example of a type reference, and in the variable
declaration of result
, size
is an example of a type reference to an integer constant.
Instantiated Type¶
An instantiated type defines an instantiation of a generic type.
-
type
Instantiated
¶ -
where there must be at least one type specified in the list of types between the angle brackets.
Consider the following variable declarations:
The variables a
, b
, c
, and d
are declared with the instantiated types Array<Bool, 3>
, Bounded<0, 4>
,
Slot<Int>
, and NonVerified<Int>
respectively.
Reference Type¶
A reference type is a reference (or pointer) to a value of that type, and is denoted using: &
for an immutable
reference; &mut
for a mutable reference, or &out
to a reference that must be written to before being read.
Reference types can be used to implement out and inout parameters for functions, such as:
function swap<T>(left : &mut T, right : &mut T) : Nil = {
val t = left;
left = right;
right = t;
}
See Functions for further details on out parameters.
Reference types can only be used in conjunction with function types or when declaring the type of a function parameter. Usage of reference types outside of these contexts will result in an static error.
Function Type¶
A function type denotes the type of a function in terms of the types of its parameters, and its return type.
-
type
Function
¶
Function types are not comparable for equality, and are not defaultable, but they are verifiable.
Consider the following example:
This function type describes a function that takes two parameters of type Int
, and returns a value
of type Bool
. As an example, the following two function declarations are of this type:
Function types that do not have any parameters are simply denoted with empty parentheses. For example:
() -> Nil
This function type describes a function that does not have any parameters, and has the return type Nil
.
Function types can be used as any other type, for example, in a variable declaration:
In this example, variable x
is declared to be of type (Int, Int) -> Bool
, and is initialised with a reference to
the function isEqual
(declared above). The function contained in x
can be called in the usual way, for example:
var y : Bool
y = x(2, 5)
Function types describing functions that have out
or inout
parameters are written in the same way, where their
corresponding types capture this information. For example, consider the following function declaration:
In this case, the function type for square
is:
In this example, the second parameter of this function type is of an out
reference type.
Function types can only be assigned references to static (i.e. non-member) functions and external functions that are
deterministic. This means that it is not possible to assign a reference to a function that
is defined within a state machine, or an external function with a body that includes the nondet
expression.
The following example illustrates how function types can be used in combination with the builtin Optional
and
Signal
types in a port state machine:
port P {
function check() : Nil
outgoing signal ok(x : Bool)
outgoing signal fail(x : Bool)
machine {
var x : Bool
var response : Optional<(Bool) -> Signal>
function setResponse() : Nil = {
x = nondet {
true,
false,
};
response = Optional.Some(nondet {
P.ok,
P.fail,
});
}
check() = {
setResponse();
send(response.value()(x));
}
}
}
In the above, the expressions P.ok
and P.fail
are both references to functions of type (Bool) -> P.Signal
that
construct the corresponding signal (of type P.Signal) without sending it.
See also
Binary Type¶
A binary type refers to a type that is defined using a binary operator.
-
type
Binary
¶
Consider the following example of a binary type, which uses the addition operator:
In the instantiated type Bounded<0, kLimit + 1>
, kLimit + 1
is an example of a binary type.
Coco currently supports integer operators in binary types only.
Unary Type¶
A unary type refers to a type that is defined using a unary operator.
Consider the following example of a unary type, which uses the integer negation operator:
var counter : Bounded<-5, 5>
In the instantiated type Bounded<-5, 5>
, -5
is an example of a unary type.
Coco currently supports the integer negation operator in unary types only.
Declarations¶
Fields¶
Many declarations in Coco consist of one or more field declarations of the form id : T
, where id
is a name
being declared of type T
. For example, components and ports are declared through lists of fields that give their
constituent parts.
-
declaration
Field
¶ [«visibility»] (val | var) «identifier» : «type» [= «expression»]
where the expression is a simple
expression
, and can only be used as a field initialiser incomponents
.
For example, a port declaration could have the following field declarations:
val oneControlPort : ControlPort
val manyControlPorts : Array<ControlPort, 5>
This declares two ports called oneControlPort
of type ControlPort
, and manyControlPorts
of type
Array<ControlPort, 5>
.
Variables¶
A variable is a named value, which can be referenced in the code. In Coco, there are two types of variables:
a mutable variable, declared with the keyword var
, where the value of the variable can be modified; and a constant
variable, declared with the keyword val
, where the value of the variable cannot be modified.
-
declaration
Variable
¶ [«visibility»] [var|val] «identifier» [: «type» | = «expression» | : «type» = «expression»]
In a declaration statement, a variable can be declared without its type, and with an initial value only. Coco will then infer the type of the variable from the initial value.
For variable declarations at the top-level of a Coco module, the expressions
must be simple ones.
If a variable is declared to be of a type T
without an initial value, and there is an instance of
Default
declared for T
, then it will be assigned the default value of T
.
For example:
Variable x
is declared as a mutable variable of type Int
. Although x
is not explicitly assigned
an initial value, there is an instance of Default
declared for Int
with the default
value 0
, and therefore x
will be assigned the value 0
. Variable y
is a constant of type Bool
, and
is explicitly initialised with the value true
.
Variables must be initialised in order. For example, the following variables declared at the top-level of a module is not valid:
Instead, the valid ordering in Coco is:
If a variable is declared to be of a type T
that does not have an instance of Default
,
then the variable declaration must either assign an initial value as part of its declaration, or it can be
declared as being of type Slot<T>
, which means that the verification will check whether the variable is
being accessed correctly or not. For example:
The variable z
is declared to be of type Slot
parameterised with type Int
, which means that
although the actual value of z
is not verified, the verification will check whether z
is accessed correctly.
See Slot
in the Standard Library section for further details on slots, and the description of
a state machine
declaration in the State Machines section regarding the restrictions of
their use in port state machines.
Note
Most of the built-in primitive types in Coco, such as Int
, Bounded
, and Bool
,
each have
an instance of Default
, and are therefore defaultable. Likewise,
more complex types in Coco, for example Array
, are also defaultable
provided their elements are recursively defaultable. See the Standard Library section for further
details.
A variable is local to the scope in which it is declared. For example, a variable declared inside a function is local to that function, and a new variable is created every time the function is called. Variables declared inside a state machine are created when the state machine is initialised, and destroyed when the state machine terminates.
Private variables (denoted by the keyword private
) can only be declared in component
declarations, and can only be of non-verified types.
See also
Static Constants¶
A static constant belongs to the declaration within which it is defined. Since these
definitions are static, it is not necessary to create an instance of the enclosing type to
access its value. In all cases, the static constant x
can be referenced as C.x
, where C
is the
enclosing type.
-
declaration
Static Constant
¶ [«visibility»] static val «identifier» : «type» = «expression»
Static constants can be declared in enum
, struct
,
port
, component
, state machine
, and event state
declarations. The following example illustrates the use of a static constant in a struct
called Table
:
The static constant kBase
in Table
can simply be referenced as Table.kBase
in other contexts, as illustrated
in the following port example:
port P {
function getTableBase() : Style
machine {
getTableBase() = Table.kBase
}
}
The function getTableBase()
will return the value Style.Oak
.
Functions¶
A function defines a reusable block of code that can be called from different places. It has a function name, which is used to call the function when required elsewhere in the code. A function can have parameters, each declared with a parameter name and type, and specifies its return type.
-
declaration
Function
¶ [«visibility»] function «identifier»(«parameters») : «type» = «expression» «parameter» := [var] «identifier» : «type»
A function will either return the value given by an explicit return statement (like other imperative programming languages), or it will return the value computed by its expression (like expression-orientated languages such as Haskell or Rust). For example:
The functions above give two equivalent definitions of the identity function, where both versions return their argument
unaltered. The first version uses an explicit return
statement, whereas the return value of the second
version is computed by evaluating the expression argument
.
Function parameters can either be passed by value or passed by reference. If a function parameter is passed by value,
then by default is is immutable in the body of the function; that is, they can be read but not written to. To allow
the parameter to be mutated, it must be prefixed by the keyword var
. Assignments to a var
parameter within the
called function are local to the called function only, and have no effect on the calling function.
The value of a parameter passed by reference, namely what it is referencing, is immutable; it is not valid
to declare a parameter of type var v : T
, where T
is a reference
type. For a parameter of type
v : T
, where T
is a reference
type, the assignment expression v = x
means the value that v
refers to is assigned the value of x
; it does not change what v
is referring to.
The values that are being referred to may be mutable depending upon the parameter’s reference
type. If
a parameter is of type &T
for some type T
, then the values being referenced are immutable, whereas if it is of type
&mut T
or &out T
, then they are mutable. The difference between the types &mut T
or &out T
is that for
a parameter of type &out T
, the value being referenced must be assigned a valid value within the called function
before it returns, and before the value can be read. Any changes made by the function to the values in either case will
persist after the function returns.
Consider the following example:
The function square(x : Int)
takes an immutable parameter passed by value called x
of type Int
,
and returns the result of evaluating x * x
. The function testSquare(x : Int, y : &out Int)
takes two parameters, the
second of which is a parameter of reference
type &out Int
. This function modifies the value of y
twice before it returns.
Member functions (for example, declared in a struct
or enum
) cannot be called on &out
parameters, unless they have been written to first. Consider the following example:
The function f1(…)
is invalid Coco, because the member function reset()
of the enum Optional<Int>
is being called on the &out
parameter x
, which has not been written to yet.
The same applies to non-mutating member functions, since they still need to be able to read the value they are being
called upon. For example, the following function f2(…)
is also invalid Coco:
The following function f3(…)
extends f2(…)
by assigning a value to x
before the member
function hasValue()
is called on it, and is therefore valid Coco:
In the previous example of the function testSquare(…)
above, the local variable v
can be removed by changing the
parameter x
to be mutable using the keyword var
as follows:
In this case, all references to x
are references to the automatically created local variable with the same name,
initialised with the value passed in the function call. In contrast, consider the following example:
This function is invalid for two reasons: the parameter y
is being read before a value has been assigned to it, and
the function returns without having assigned a value to y
.
When a function is called, the arguments corresponding to parameters of a reference
type
must be prefixed with &
, with the exception of when references are forwarded as the arguments in a function call:
&
must be applied directly to a variable, and cannot be applied to arbitrary expressions.
The following example illustrates the case where a function is called, and is passed a reference as its argument:
In this example, function f
is calling g
, and passing on the reference to its &out
parameter. In this case, it is
not necessary to prefix the argument x
with &
when calling g
, because x
is declared to be of the same type
&out
in f
. It is therefore valid to simply write g(x)
instead.
Functions can take functions as parameters, and return functions. For example:
The function compute
takes two parameters, the second of which is function of type (Int) -> Int
, and returns the
value of calling y(x)
. The following example illustrate how this function can be called passing a
function reference as its argument:
The following example illustrates how a function can take functions as arguments, and have a function type as its return type:
In this example, the function f1
has the return type (&out Bool) -> Nil
, and returns a reference to the function f2
in its function body. The function f2
takes a parameter of type &out Bool
, which is assigned the value true
in
its function body, and has a return type Nil
. The function f3
does not have any parameters, and returns a value of type
Bool
. In its body, it declares two variables, vF1
, which is initialised with a reference to the function
f1
, and vF2
, which is initialised with the function stored in vF1
. The function f3()
then calls vF2(&b)
, and returns
the value of b
, which in this case will be the value true
.
Functions which are directly or indirectly called recursively cannot call functions on required ports, emit signals,
or use nondet
expressions.
See also
Note
The parameters of a function must be value types or references to value types; i.e. functions, ports, and components cannot be passed as parameters.
Function Interfaces¶
A function interface declaration is used to declare a function in a port declaration.
-
declaration
Function Interface
¶ [«visibility»] function «identifier»(«parameters») : «type» «parameter» := «identifier» : «type»
For example:
Signals¶
A signal declaration is used to declare a signal in a port declaration.
-
declaration
Signal
¶ [«visibility»] outgoing signal «identifier»(«parameters») «parameter» := «identifier» : «type»
For example:
outgoing signal ok()
outgoing signal error(status : Bool)
Enumerations¶
An enumeration, also called an enum, defines a type with a finite number of named values, referred to as enum cases. In Coco, enum cases can also have data values associated with them. These are referred to as tagged enums, and are effectively a type safe union.
-
declaration
Enum
¶ [«visibility»] enum «identifier» { «enum elements» } [«visibility»] «enum element» := «enum case» | [mut | static] «function» | [mut | static] «external function» | «static constant» «enum case» := case «identifier»[(«parameters»)] [= «expression»] «parameter» := «identifier» : «type»
where if an enum case has parentheses, then it must have at least one parameter inside them.
The name of an enum becomes the name of its type. An enum case is referenced elsewhere by using a dot (.
) between
the enum type name and its case name, as illustrated in the examples below. An enum can also
have member functions, which can be called on an instance x
of the enum by using a dot (.
), for example, x.fn()
.
If a member function is preceded by the keyword mut
, then it means that the function is permitted to modify the
value of the enum. Member functions, including external ones, can be static
, which means they are
defined on the type, as opposed to an instance of the type. An example of a static function can be found in
struct
.
An enum where none of its cases have parameters is called a simple enum, whereas an enum that has at least one enum case with parameters is called a tagged enum.
Simple enums are always comparable for equality, and defaultable with the default value being the first
enum case listed in the declaration. Coco will automatically generate instances of Eq
and
Default
without having to label them with the @derive
attribute. For example:
enum SimpleColour {
case Red
case Blue
case Green
}
function isDefaultColour() : Bool = {
val v : SimpleColour;
return v == .Red;
}
The function isDefaultColour()
will return true
, since the variable v
is assigned the default value Colour.Red
when it
is declared.
By default, tagged enums are not comparable for equality, but they can be labelled with @derive(Eq)
so that Coco
automatically derives an instance of Equality
when every parameter type in every enum case has an instance of
Eq
. Likewise, by default tagged enums are not defaultable, but derive(Default)
can be used to
automatically derive an instance of Default
when all of the parameters of the default enum
case are defaultable, where the default enum case is the first enum case listed in the declaration.
The following example is of a tagged enum, where one of the enum cases has parameters to represent different colours:
In this example, Colour
is a tagged enum where the first enum case RGB
has three parameters of type Int
representing
the decimal colour codes, and the second enum case None
does not have any parameters, representing the case
where no colour is specified. Colour
is defaultable with the default value .RGB(0, 0, 0)
, and is
comparable for equality since every parameter is of type Int
, which in turn is comparable for equality.
The following example illustrates how member functions can be declared in an enum declaration:
The following simple functions illustrate how a variable can be declared to be of type ErrorStatus
, and its
member functions called:
The function hasError1()
always returns true
, because the variable error
is assigned the value
.Error(3)
, and therefore the member function error.status()
returns true
. In contrast, the function
hasError2()
always returns false
, because after the variable error
is assigned an error value, and before the function
returns, error
is reset to .None
as a result of calling error.reset()
.
See also
Structures¶
A structure, also called a struct, combines several types into a larger compound type, giving each value within the structure a name.
-
declaration
Struct
¶ [«visibility»] struct «identifier» { «struct elements» } [«visibility»] «struct element» := «field» | [mut | static] «function» | [mut | static] «external function» | «static constant»
The name of the struct becomes the name of its type. If x
is a variable of a struct type S
, where
S
has a field f
with type T
, then x.f
can be read from, and assigned a value of type T
. A struct S
can
also have member functions, which can be called on an instance x
of the struct by using a dot (.
), for example,
x.fn()
. If a member function is preceded by the keyword mut
, then it means that the function can modify the
values assigned to fields in the struct. Member functions, including external ones, can be static
, which means they are
defined on the type, as opposed to an instance of the type.
By default, structs are not defaultable, but they can be labelled with @derive(Default)
so that Coco
automatically derives an instance of Default
when the type of every field has an instance
of the class Default
. Likewise, by default structs are not comparable for equality, but
derive(Eq)
can be used to automatically derive an instance of Eq
when the type of every
field has an instance of the class Eq
. See
@derive
for further details regarding which traits can be derived for structs.
Consider the following example of a struct called Message
, which implements a simple buffer, together with
member functions to read from, write to, and reset it:
val size : Int = 2
type Buffer = Array<Int, size>
@derive(Eq)
struct Message {
var store : Buffer
var index : Int
static function initial() : Message = Message{ store = [0, 0], index = 0 }
mut function reset() : Nil = {
for i in range(0, size) {
store[i] = Int();
}
index = 0;
}
mut function write(data : Int) : Bool = {
if (index < size) {
store[index] = data;
index = index + 1;
return true;
}
return false;
}
function read() : Optional<Buffer> = {
if (index == size) {
return Optional.Some(store);
}
return Optional.None<Buffer>;
}
}
The function reset()
is an example of a mutating member function, as it resets all of the array elements to the default
value of Int
. In contrast, the function read()
is not, as it simply returns the value of store
in the
case where it has been written to, or Optional.None<Buffer>
otherwise.
The function initial()
is an example of a static function, as it is defined on the type Message
, as opposed to an
instance of it. The following example illustrates how this function can be used to set the default value for a variable
declaration:
var m : Message = Message.initial()
The struct Message
is labelled with @derive(Eq)
, which means that Coco will try to automatically derive the instance of
the trait Eq
for this struct. This is necessary to compare instances of Message
for equality, for
example as illustrated in the following function isEqual(…)
, which returns .Ok(nil)
if the two arguments of type
Message
are equal, and .Error(y)
otherwise, where y
is the value of the second argument:
The function run(…)
returns the value .Ok(true)
if all of its arguments are equal. If either of the
expressions of the form isEqual(a, b)?
evaluates to .Error(b)
, then run(…)
will immediately return with the value
.Error(b)
.
Type Aliases¶
A program may give new names to existing types by using type aliases.
-
declaration
Type Alias
¶ [«visibility»] type «identifier» = «type»
The newly named type behaves identically to the type for which it is an alias. However, it may have different attributes attached to it, so two otherwise identical types with different names may be treated differently by the code generation or verification tools.
The following example illustrates how a type alias can be defined and used:
type TemperatureDegrees = Int
function warm(t : TemperatureDegrees) : TemperatureDegrees = if (t < 40) t + 1 else t
External Functions¶
Coco provides a range of language constructs to support the integration of the generated code into the rest of the system. One of these is an external function declaration, which allows Coco programs to reference functions that are defined by some other implementation outside the scope of the Coco program, but that the Coco generated code will be able to use.
-
declaration
External Function
¶ [«visibility»] external function «identifier»(«parameters») : «type» = «expression» «parameter» := [var] «identifier» : «type»
The body of an external function is an expression, which cannot use any non-verified types.
An external function is similar to a port in the sense that it provides the specification of the function, not its implementation. The body of an external function is only used during the verification; no code is generated from it.
Consider the following simple example:
enum Command {
case Start
case Stop
}
external function getCommand() : Command =
nondet {
.Start,
.Stop,
}
The external function getCommand()
returns a value of type Command
. The return value is chosen nondeterministically,
and the possible return values will be taken into account when verifying calls to the function.
The following function declaration calls the external getCommand()
function in a match
:
If the return type of an external function is a non-verified type, then the undefined
expression (_
) must be used as the return value:
external function errorMessage() : String = _
In this example above, the external function errorMessage()
returns a value of type String
, which is a non-verified type,
and therefore the body of the function must be _
.
When an external function is declared with an &out
parameter, Coco requires the &out
parameter to be written to, and
therefore assumes the code implementing this function will write a valid value. This has to be specified in the body of the
external function. For example:
If an &out
parameter is of a non-verified type, then it must be assigned the undefined
expression (_
) in the external function’s body, as illustrated in the following example:
external type Data
external function getData(v : &out Data) : Nil = {
v = _;
}
In this example, getData(...)
has an &out
parameter v
of external type Data
, and is therefore a non-verified type.
The assignment v = _
in the function body means that the function will write an arbitrary but valid value to
its &out
parameter before it returns.
The assumption that &out
parameters are always written to is
particularly useful when passing a reference to a Slot<T>
to a function that has an &out
parameter of type T
, since
it means that the slot can be assumed to be valid by the time the function returns. To illustrate this,
consider the following example:
In this example, the function getDuration()
converts an Int
into a Duration
, which can
then be used in timer transitions
in a component state machine. When the local
variable timeout
is declared as a Slot<Int>
, its default value is SlotStatus.Invalid
, indicating that the slot does
not contain a valid value at this stage. However, it is guaranteed to be assigned a valid value upon return
of getTimeoutValue(&timeout)
.
Note
The verification checks that the value of a slot is never used when invalid. For example, if the call to
getTimeoutValue(&timeout)
was omitted in the example above, and getDuration()
was called in a state machine
then the verification would fail with the error that it was returning a value that had not been written to.
If an external function has a parameter that is passed by value, mutable, and of a non-verified type,
then it should be assigned the undefined
expression (_
) in the function body, as illustrated in the
following example:
external function f1(var d : NonVerified<Int>) : Nil = {
d = _;
}
External Types¶
An external type declaration allows users to refer to a type that is not defined within their Coco program, but is defined somewhere else in the code. External types help integration of the generated code into the rest of the system, and also allow users to provide information about these types for the purposes of verification.
-
declaration
External Type
¶ [«visibility»] external type «type» [{ «external type elements» }] [«visibility»] «external type element» := «static constant» | «variable declaration» | [mut | static] «external function» | [mut | static] «function»
where variable declarations must be of a type that has an instance of
Verified
, and can only be accessed by member functions.
Like external functions, the body of an external type is only used during the verification; no code is generated from it.
By default, a user-defined external type is not defaultable. However, if the real implementation of the type in
the target language is defaultable (for example, in C++, if the type is default-constructible), then its declaration can be
labelled with with the @derive
attribute, written as follows:
Users can also specify that a custom external type is defaultable by declaring an instance of
Default
for that type. However, this approach is more restrictive as the trait instance can only use
external functions
, and the body of those functions must be simple.
A variable can be declared to be of some external type T
. If the external type is not defaultable, then
Slot
or Optional
of type T
can be used instead.
Note
A useful consequence of how external types are handled in Coco is that a value of some type T
can always be assumed
to be a valid well-defined value of type T
, even when T
is an external type and therefore not verified. This is
achieved by requiring that either there is an instance of Default
for T
, which means that it has a
valid value from the start, or the variable of type T
is initialised.
By default, external types are not verified, and therefore no data about them is stored during verification.. However, users can some information about an external type in body of its declaration, in order to enable some verification to be carried out. This is an abstract specification, since it is only used for verification purposes, and no code is generated from it. For example:
This is an example of a Coco external type declaration for a vector. Although the contents of the vector are not
verifiable, the size of the vector is and can therefore be used during verification. The variable
size_
stores the current size of the vector, and is implicitly initialised with the value 0
. It can be accessed
via calls on the external type’s member functions, for example, the function clear()
resets the value of size_
to
0
. The following function testVector()
illustrates how a variable of type Vector
can be declared, and its
member functions called:
This passes verification, which means that all assertions pass.
External types can be mapped to existing definitions elsewhere in the code using the @mapToType
attribute, for example:
This is an example illustrating how Vector
can be mapped to an existing definition in C++.
Member functions can also be mapped to their implementations using the @mapToMember
attribute,
as illustrated in the following example where the target language is C++:
@CPP.mapToMember("clear")
mut external function clear() : Nil = {
size_ = 0;
}
In addition to making it easier to integrate the generated code into the rest of the system, Coco’s
external type
is a powerful tool for allowing partial verification
involving variables of external types that are not verifiable by default.
External Constants¶
Constants defined outside of Coco can be referenced in Coco by declaring them as external constants.
-
declaration
External Constant
¶ [«visibility»] external val «identifier» : «type» = «expression»
External constants must be declared with a value, and are used for verification purposes only. For example:
external val kLimit : Int = 4
This declares the external constant kLimit
of type Int
with value 4
. During verification,
Coco will assume that it has the value 4
.
External constants can be declared to be of an external type. For example:
external type DataPacketSize
external val packetSize : DataPacketSize = _
External constants of an external type must be initialised with the undefined
expression (_
)
as illustrated above, unless the external type has a body, in which case it must be initialised with a
value of the variable type declared in the external type’s body. For example:
external type BlockSize {
var size_ : Bounded<0, 128>
}
external val defaultBlockSize : BlockSize = BlockSize{ size_ = 16 }
Note
The value assigned to an external constant in its initialiser is used for verification purposes only, and is not assumed or checked at runtime.
External constants can be labelled with the @mapToValue
attribute in order to map them to their
corresponding definition in the generated target language, as illustrated by the following example:
@CPP.mapToValue("cocotec::project::kTimeoutPeriod", .User("cocotec/constants.h"))
external val kTimeoutPeriod : Duration = _
In this example, the external constant kTimeoutPeriod
is mapped to a constant value declared in C++.
Expressions¶
Coco is an expression-oriented language, and so most constructs within the imperative language are expressions. Evaluating an expression gives a value, and may also cause side-effects.
-
expression
Expression
¶ «expression» := «array subscript expression» | «array literal expression» | «assignment expression» | «binary operator expression» | «block expression» | «cast expression» | «compound await expression» | «default constructor expression» | «function call expression» | «if expression» | «implicit member expression» | «inherit expression» | «literal» | «match expression» | «member reference expression» | «nondet expression» | «optional expression» | «signal send expression» | «struct literal expression» | «try operator expression» | «unary operator expression» | «undefined expression» | «variable reference expression»
A simple expression is one that has no control flow, and therefore excludes: block
, if
,
match
, nondet
, optional
, and try operator
expressions.
All subexpressions in an expression are evaluated in order from left to right.
Variable Reference¶
One of the basic building blocks in Coco is a variable expression, which allows existing declarations to be referenced.
-
expression
Variable Reference
¶
Member Reference¶
A member reference expression is used to reference the value of a field within an instance of some type.
-
expression
Member Reference
¶
For example, consider the following struct
declaration:
Given a variable declaration called date
of type Date
, date.day
refers to the value
of the field day
within the instance of the variable date
.
Implicit Member¶
An implicit member expression is an abbreviated way of accessing a member of a type, without having to explicitly specify the type. This can be used in contexts where the type can be automatically inferred.
-
expression
Implicit Member
¶
For example, consider the following simple enum
:
enum SimpleColour {
case Red
case Blue
case Green
}
function isDefaultColour() : Bool = {
val v : SimpleColour;
return v == .Red;
}
In the function isDefaultColour()
, the implicit member expression .Red
can be used as an abbreviation for
SimpleColour.Red
, since its type can be inferred from the type of v
.
Implicit member expressions can also be used in other contexts, such as:
- Builtin attributes, for example
@runtime(.MultiThreaded, .SingleThreaded)
,@queue(.Unbounded(1))
, and@ignoreWarning(.DeadState)
; - Queue control functions, for example
setAllow(r, .sigA)
andsetUnique(r, .sigA)
; setAccess
functions, for examplesetAccess(r, .Shared)
.
Struct Literal¶
An struct literal expression is used to assign values to fields in a struct
.
-
expression
Struct Literal
¶ «expression» { «field assignments» } «field assignment» := «identifier» = «expression»,
The following example illustrates the use of a struct literal:
struct ColourCode<T> {
var red : T
var green : T
var blue : T
}
function red() : ColourCode<Int> = ColourCode{ red = 255, green = 0, blue = 0 }
Operators¶
Coco provides a range of operators, which are commonly found in other programming languages. These include binary and unary operators for integer and boolean types, each described in turn below.
Binary Operators¶
A binary operator operates on two operands, for example:
1 + 2
1
and 2
are the operands, and +
is the binary operator for addition. The binary operator is an infix operator,
because it appears between the two operands.
-
expression
Binary Operator
¶ «expression» «operator» «expression»
The operator is either an integer or boolean operator. For binary integer operators, the expressions must evaluate to a
value of a numeric type, where a type is numeric precisely when it has an instance of the trait Num
. For
example, Int
, UInt
and Bounded
are numeric types. For binary boolean operators, the expressions must
evaluate to a value of type Bool
.
Coco supports the following binary integer operators:
Operator | Meaning |
---|---|
+ |
Addition |
- |
Subtraction |
* |
Multiplication |
/ |
Division |
% |
Remainder |
For example:
6 + 2 is equal to 8
6 - 2 is equal to 4
6 * 2 is equal to 12
6 / 2 is equal to 3
6 % 2 is equal to 0
These binary integer operators are defined for any type that has an instance of the trait Num
. For the remainder
operator to be supported, the type must also have an instance of the trait Remainder
. The builtin types
Int
, UInt
, and Bounded<…, …, Int>
implement both of these traits.
The remainder operator (x % y
) takes two operands x
and y
, and gives the remainder resulting from dividing x
by
y
. The remainder is calculated using the following equation:
x = (y * m) + r
where m
is the maximum number of multiples of y
that fit into x
, and r
is the remainder. For example, assume
x
is 13
and y
is 4
. The maximum number of multiples of y
that fit into x
is 3
, and therefore 13 % 4
is
equal to 1
:
13 = (4 * 3) + 1
The remainder operator is also referred to as the modulo operator in some other languages. However, its implementation
can vary across the different languages when it comes to dealing with negative integers. In Coco, the remainder operator
for x % y
ignores the negative sign for values of y
, and therefore x % y
is always equal to x % -y
. In the case
where x
has a negative value, then the equation above applies. For example, if x
is -13
and y
is 4
then:
-13 = (4 * -3) + -1
Therefore, the remainder would be -1
.
Note
The result of the remainder operator has the same sign as the dividend, as it is in Java.
Coco also supports the following binary boolean operators:
Operator | Meaning |
---|---|
== |
Equality |
!= |
Inequality |
|| |
Logical OR |
&& |
Logical AND |
< |
Less than |
<= |
Less than or equal |
> |
Greater than |
>= |
Greater than or equal |
The operators equality (==
) and inequality (!=
) can only be used if there is a valid instance of
Eq
declared for the type of their first argument respectively.
The operators less than (<
), less than or equal (<=
), greater than (>
), and greater than or equal (>=
) can only
be used if there is a valid instance of Ord
declared for the type of their first argument
respectively.
As with many C-like languages, &&
and ||
short-circuit and only evaluate their second argument if required.
For example, when evaluating e1 && e2
, e2
is only evaluated if e1
evaluates to true
.
Unary Operators¶
A unary operator operates on a single operand, for example -x
, where -
is the unary operator for integer negation.
-
expression
Unary Operator
¶ «operator» «expression»
Coco currently supports the following three unary operators:
Operator | Meaning |
---|---|
- |
Integer negation |
! |
Logical negation |
& |
Reference |
When using the integer negation operator, the expression must evaluate to a value of a numeric type that has an instance
of the trait Negatable
. A type is numeric precisely when it has an instance of the trait Num
.
For example, the builtin types Int
and Bounded
are numeric types, and Int
and Bounded<…, …, Int>
both have an instance of Negatable
. In contrast, UInt
is also a numeric type, but does not
have an instance of Negatable
. For the logical negation operator, the expression must evaluate to a
value of type Bool
; and for the reference operator, the expression must be an lvalue expression.
Operator Precedence¶
Operator | Associativity |
---|---|
= |
Right |
|| |
Left |
&& |
Left |
== != |
Left |
>= <= > < |
Left |
* / % |
Left |
as |
Left |
& - ! |
Try Operator¶
The try operator, written as ?
, can be used to simplify error handling by reducing the amount of
code used to propagate errors in the case of functions that return either the Result
or
Optional
type.
Given an expression x?
, where x
evaluates to a value of type
Result<T, E>
, if x
evaluates to .Ok(v)
for some value v
of type T
, then x?
will evaluate to v
. Otherwise, if x
evaluates to .Error(e)
for some error
value e
of type E
, then x?
will cause the calling function to immediately return with the
value .Error(e)
.
For an expression of the form e : Result<T, E>
, a function of the form f(e?)
is equivalent to the
following expanded version using match
:
f(match (e) {
.Error(x) => { return .Error(x); },
.Ok(v) => v,
})
This illustrates why a function that evaluates an expression of the form e?
, where e
is of type Result<T, E>
, must also have return type Result<S, E>
.
Note that although the parameter types T
and S
may differ, the
parameter type E
for the error case must be the same in both cases in order to handle the
early return of the enclosing function. This is illustrated in the following examples below.
The following function is an example of one that takes an argument x
of type Int
,
and returns a value of type Result<Int, Nil>
:
When check(v)
is called, it returns .Error(nil)
if v
is equal to 0
, and .Ok(v)
otherwise.
The following example illustrates how a function can use the try operator when calling check
:
When evaluating check(x)?
and check(y)?
, if either of them evaluates to .Error(nil)
, then
the function sum
will immediately return .Error(nil)
. Otherwise, it will assign the value resulting
from x + y
to z
, and return .Ok(true)
.
The builtin enum Result
is labelled with the attribute @mustUse
. This means that
when a function evaluates an expression e
of type Result
, it must also use a corresponding
value of e
, as illustrated with the following modified version of the example above:
If either check(x)
or check(y)
was accidentally called without ?
, then Coco would give an error
pointing out that the value is of type Result<Int, Nil>
, and must be used due to the attribute
@mustUse
on Result
.
The try operator can also be applied to expressions that evaluate to a value of type Optional
.
For an expression of the form e : Optional<T>
, a function of the form f(e?)
is equivalent to the following expanded
version using match
:
f(match (e) {
.None => { return .None; },
.Some(v) => v,
})
A function that evaluates an expression of the form e?
, where e
is of type Optional<T>
, must also have return type Optional<S>
. As is the case for
Result
, T
and S
may differ. Unlike Result
, the error case resulting in the
early return of .None
does not have a parameter, and therefore can always be
handled by an enclosing function that has return type Optional<S>
for any type S
.
The following functions illustrate the use of ?
and Optional
together:
In this example, if any of the calls to step
result in .None
, then the enclosing function
multipleSteps
will immediately return with the value .None
. Otherwise, it will return with
the value .Some(v)
, where v
is the sum of arguments given for x
, y
, and z
.
Block¶
A block expression comprises a sequence of semicolon-separated statements, and optionally an expression that describes a
block of code. When the block gets evaluated it will either give the value nil
, or the value of the
expression at the end of the block.
-
expression
Block
¶ { «statements» [«expression»] }
The final expression, if present, has no trailing semicolon. The value and type of the block are those of the
final expression. If the final expression is absent, the value of the block expression is nil
.
The following function is an example of one where the value of the block expression is given by the final
expression y
:
In contrast, the following function is an example of one with a block expression that does not end with an expression:
In this case, the value of the block expression, and hence the function is nil
.
The following example is of a function with a block expression that also does not end with an expression, but instead has
an explicit return
statement:
In this example, the result of the function is returned explicitly and is of type Int
.
Assignment¶
An assignment expression is used to assign values to variables.
-
expression
Assignment
¶
The value of a mutable variable can be modified using an assignment
expression, for example:
Arrays¶
An array is a container type in Coco, and stores values of the same type in an ordered list. Coco
supports two types of arrays called Array
and SymmetricArray
, each described in turn below.
The Array
type is the standard and most commonly used one, where the size of the array is the actual size
used for the verification and runtime. For example:
Variable y
is declared as an array of length 4
, and contains values of type
Int
. It has not been provided with an initial value as part of its declaration, and therefore all of the
elements in the array will be given the default value 0
of their type Int
.
In contrast, SymmetricArray
is a special array type that can only be used in implementation components
to declare an array of provided ports, where the component treats each index of the array identically.
They are written as follows:
val client : SymmetricArray<Provided<P>, 100>
The variable client
is a symmetric array of provided ports of type P
. The size of the array (in this case, 100
)
is only relevant at runtime. For verification, Coco will automatically compute the minimum array
size it needs to verify in order to guarantee that the component will work correctly for an array of any size greater
than this minimum threshold. This allows the verification of components with a large number of provided ports that are
treated symmetrically to be optimized. See Components with Symmetric Provided Ports for further details and examples.
There are two expressions used to access the elements in an array, namely an array literal
, and an
array subscript
.
-
expression
Array Literal
¶ '['«expressions»']'
where the expressions in the square brackets is a comma-separated list of expressions each of which must evaluate to a value of the same type.
When an array literal is used to initialise an array, the type of the expressions in the array literal must match the type of the array being declared. For example:
-
expression
Array Subscript
¶ «expression»'['«expression»']'
In Coco, array subscripting starts with 0
, and therefore the first
location in an array a
is a[0]
. The subscript expression used for an Array
must evaluate to a value of
a type that has an instance
of Integer
. In contrast, the subscript expression used for a SymmetricArray
must of type
SymmetricArrayIndex
. This
imposes certain restrictions on how values of this type are used, which are required to ensure soundness of the
verification. See SymmetricArrayIndex
for further details and examples.
The array subscript expression is used to access the values stored in an array. For example, using variable x
in the
example above, we can access the values of its elements using an array subscript expression:
val c : Bool = x[0];
c
is declared to be a constant variable of type Bool
, and is initialised with the value stored in
location 0
of the array x
as its initial value.
Coco also supports multi-dimentional arrays. For example:
The value of m[0]
is [false, false]
, and the value of m[0][0]
is false
.
Array subscript expressions can also be used in assignment
expressions to modify the values in an array.
For example:
x[0] = Bool();
y[3] = 5;
m[0][1] = true;
where x
, y
, and m
are declared in the previous examples above. In these assignment expressions, x[0]
is
assigned the default value of Bool
, y[3]
is assigned the value 5
, and m[0][1]
is assigned the value true
.
Function Calls¶
A program can use a function by making a function call. A function call comprises the function’s name and arguments that match the order and types of the parameters in the function declaration. The arguments are evaluated from left to right before being passed to the function.
-
expression
Function Call
¶
A function call expression evaluates the result returned by the function, and the type of the expression is the return type of the function.
The following example is of a simple function, which returns the sum of two integers, and a function testSum()
,
which calls sum(2, 3)
:
Sending Signals¶
A program can send a signal to other parts of the system. The syntax for sending a signal is the same as for
a function call
expression.
-
expression
Signal Send
¶
where the type of the signal send expression is Nil
.
A signal send expression is used in state machines to describe the communication between components. For example:
port P {
function checkStatus() : Nil
outgoing signal ok()
machine {
checkStatus() = ok()
}
}
This simple port P
declares a function called checkStatus()
, and an outgoing
signal called ok()
. Its state machine specifies that when checkStatus()
is called, it will send the signal ok()
.
Conditionals¶
Coco has two types of conditional expressions, if
and match
, which enable different
branches of code to be
executed depending on certain conditions.
If¶
An if expression has a condition, and a branch expression that gets evaluated if the condition evaluates to true
.
If the condition evaluates to false
, then it will evaluate the expression of the else
clause if there is one, or
evaluate to nil
otherwise. In Coco, an if expression that does not have an else
clause has type Nil
,
regardless of the value of the branch expression.
-
expression
If
¶ if («expression») «expression» [else «expression»]
where the condition expression must evaluate to a value of type
Bool
.
The following three functions have the same behaviour, and illustrate the use of an if expression to toggle the values passed to the functions:
Note
Since if
is an expression in Coco, it can be used, for example, on the right-hand side of an
assignment, in contrast to many other languages.
Match¶
A match expression is similar to a switch statement in C-like languages. It has an expression, the value of which is compared against a list of possible matching patterns, and then evaluates the expression of the first pattern that correctly matches the value.
-
expression
Match
¶ [«identifier»] match («expression») { «match clauses» } «match clause» := «pattern» [if («expression»)] => «expression»,
where the match expression must evaluate to a value of type
Bool
,Int
orenum
. The expression in the optional guard must be a simpleexpression
, and must evaluate to a value of typeBool
.
A match evaluates the match expression, and matches the value to each match clause in sequence from top to bottom. It then
evaluates the clause expression of the first pattern that matches the value. It is a dynamic error if there
are no clauses that match the match expression. Thus, every match without
an explicit wildcard clause is the same as if it had _ => abort()
added as the last clause.
The following functions illustrate different examples of match
expressions:
The following example illustrates how values of a variable within an enum
can be extracted in
a match
expression:
This example also illustrates the use of a guarded enum case
pattern in the third match clause, which
means that it only matches this pattern in the case where id < 3
evaluates to true
.
When used as a statement, a match
can be labelled with an identifier
, which in turn
can be referenced by a break
statement. For example:
In this example, the first match
expression is labelled with L1
, and the second one, which is nested within the
catch-all clause, is labelled with L2
. These labels are referenced by two of the break
statements to
identify which match statement must be terminated.
Nondeterministic Choice¶
Coco has a nondet expression, which enables different branches of a program to be executed, the choice of which is decided nondeterministically. This type of expression is essential for expressing port state machines, which describe partial views of behaviour that are visible across a component (e.g. software) boundary.
-
expression
Nondet
¶ nondet { «nondet clauses» [otherwise «expression»] } «nondet clause» := [if («expression»)] «expression»,
where the guard expression in the nondet clause must be a simple
expression
, which evaluates to a value of typeBool
, and is strictly side-effect free. If the expression in the nondet clause is ablock
expression, then the semicolon at the end of the clause is omitted. A nondet must have at least one nondet clause.
A nondet expression will execute one of the nondet clauses that either has a guard that evaluates to
true
, or no guard. The choice of which clause is chosen is made nondeterministically.
If there are no such clauses, then the fallback expression, denoted by the keyword otherwise
, will be evaluated.
Consider the following example of a simple port:
port P {
function checkStatus() : Bool
machine {
checkStatus() = nondet {
true,
false,
}
}
}
Port P
declares a single function checkStatus()
, which returns a value of type Bool
. Its state
machine specifies that when checkStatus()
is called, it will return true
or false
nondeterministically.
It is a dynamic error for a nondet expression to have no nondet clauses available, and no otherwise expression.
Optional¶
An optional expression is one that nondeterministically decides either to perform a given expression, or do nothing. A
common use of optional is in a transition’s event handler
in a port state machine, for example to
specify that upon receiving a function call, either a signal is sent or no action is taken, the decision of which is
nondeterministic. Expressing this choice as an optional expression is more efficient than using a
nondet
expression, as the latter would require the explicit addition of the nondet clause to represent the
no action option (i.e. {}
).
-
expression
Optional
¶ optional «expression»
where the expression must evaluate to a value of type
Nil
.
If the optional argument is not a nondet
expression then:
optional «expression»
is semantically equivalent to:
nondet { «expression» , {} }
In contrast, if the optional argument is a nondet
expression then:
optional nondet {
«nondet clauses»
}
is semantically equivalent to:
nondet {
«nondet clauses» , {}
}
For example, the expression:
optional nondet { e1, e2, e3, }
is semantically equivalent to:
nondet { e1, e2 , e3 , {} }
A nondet
expression in an optional argument cannot have an otherwise
clause.
Consider the following example of a simple port P1
:
port P1 {
function check() : Nil
outgoing signal error()
machine {
check() = optional error()
}
}
When the function check()
is called, the state machine will either send the error()
signal or do nothing; in both
cases it will stay in state Main
where check()
can be called again. The event transition
for check()
is equivalent to writing the following:
check() = nondet {
error(),
{
},
}
Consider the following port P2
, where the signal is extended with a boolean parameter, and the expression passed to
optional
is a nondet
expression:
port P2 {
function check() : Nil
outgoing signal error(status : Bool)
machine {
check() = optional nondet {
{
error(true);
setNextState(Terminated);
},
error(false),
}
}
}
In P2
, when check()
is called, the state machine will nondeterministically choose to either: send the
signal error(true)
, and transition to the Terminated
state; send the signal error(false)
, and remain in the same
state; or do nothing, and remain in the same state.
See also
The @eventually
attribute can be applied to the argument of an optional
expression.
See the eventually section for further details.
Compound Await¶
A compound await expression allows a component state machine to block until certain events
occur. A common use case is within the body of a function transition, where the state machine has to wait for certain
signals before the function is allowed to return to its caller. Coco also supports simple await
statements,
which is a variant of await for a single event
.
-
expression
Compound Await
¶ Standard: 1.1 [«identifier»:] await [ («expression») ] { «await-clauses» } «await-clause» := «event» [ if ( «expression» ) ] => «expression»,
An await expression contains await clauses, each of which consists of an event
that it
is waiting for, and an expression that is the value of the await if this event occurs. An await can optionally
have a stop condition, denoted by the parenthesised expression immediately after await
, which specifies when the
await has completed. It can also be optionally labelled with an identifier immediately preceding await
, which can
be referenced by a break
statement in one of the clauses.
Each await clause is defined as resolving the await, or not. If resolved, then the await is considered to be finished and has the resulting value of the corresponding clause evaluated. If a clause does not resolve the await, then the expression of the clause is still evaluated, but the await will continue waiting for the next event.
The event
in an await clause must either be a signal receive event
,
a receive fallback event
, an empty event
, or a timer event
,
each described in turn below.
A receive event
in an await clause matches a signal received, as illustrated in
the following example:
machine {
client.fn() = {
await {
source.sig1() => {
return true;
},
source.sig2() => {
return false;
},
};
}
}
In this example, an await expression is used in the body of a function transition. When client.fn()
is called,
the state machine will be blocked waiting for the await to be resolved. When either the signal sig1()
or sig2()
is
received, the matching await clause is fired, the corresponding return
is executed, and the
await is resolved.
An await clause with a receive event
resolves the await, unless there is a stop condition, or the value
is inherit
(described below).
A receive fallback event
matches any signal that is not matched by any other clauses (similar to the
otherwise
clause in an offer
). It must appear last in the list of clauses, and when fired, the await is
not resolved.
An await clause with the empty event
cannot be guarded, is fired when the component’s queue is empty, and
always resolves the await, regardless of whether the await has a stop condition or not.
An await clause with a timer event
will fire after the given time period lapses. Timers are started
when the await is entered and are stopped when the await is resolved. Like timer transitions
,
an after
timer fires at most once, whereas periodic
timer will continuously fire whilst the await is active.
Thus, await clauses with periodic
timers do not resolve the await, whereas those with after
timers always do, regardless
of whether the await has a stop condition or not.
Unless specified otherwise, await clauses can optionally have guards, indicating when they are enabled. When a particular event occurs, the await clauses are inspected to find a clause whose event matches; if there is not precisely one matching clause, then this is a verification error.
An await expression can have a stop condition, which is another way of defining when the await has become
resolved. The stop condition is an expression
, which must evaluate to a value of
type Bool
. If true
, then the await is resolved. The stop condition is evaluated first before any
clauses are executed, and then each time an await clause is fired. Note that this means that await (true)
will
immediately resolve without blocking for any events. An await expression with a stop condition is defined to be of
type Nil
.
The following example illustrates the use of an await expression with a stop condition to block a component state machine within the execution of a function call until all of its required ports have successfully completed their actions:
Following a call on client.start
, this state machine calls start
on each of its required ports (in an array),
and then executes await
. Upon entry, the stop condition evaluates to false
, and therefore the
state machine is blocked waiting for one of its clauses to be executed. When it receives the signal isDone(true)
from
both of its required ports, the stop condition evaluates to true
thereby resolving the await.
Coco has a special expression called inherit, which can be used as the value of an await clause whose event is
either a receive event
or receive fallback event
.
-
expression
Inherit
¶ inherit
An await clause with inherit
means that the event should instead be
processed by the main state machine, rather than by the await
expression. When this
happens, the event is dispatched into the enclosing state machine, and the event will be matched from the current
state of the state machine in the usual way.
Consider the following example of a component state machine:
machine {
client.fn() = {
source.fn();
return await {
source.sig1(x) => x,
source.sig2(_) => inherit,
};
}
source.sig2(_) = {}
}
In this example, the function client.fn()
returns an await expression, where the second clause evaluates to inherit
.
This means that when the await receives the signal source.sig2(_)
, the matching event transition in the enclosing
state machine will be executed instead in the usual way. When executed, the await does not get resolved, and therefore
the state machine remains blocked in the await expression until it receives a source.sig1(x)
signal.
The following example illustrates the use of inherit
in a component state machine with hierarchical
states:
machine {
state Operational {
entry() = {
source[1].fn();
setNextState(Waiting);
}
client.fn() = {
source[0].fn();
return await {
source[0].sig1(x) => x,
_ => inherit,
};
}
state Waiting {
source[1].sig1(_) = setNextState(Finished)
}
state Finished {}
}
source[_].sig2(_) = {}
}
In this example, the await clause will handle the sig1()
event from source[0]
, but all other events will get handled
by the main state machine. When client.fn()
is called, the state machine will be in the state
Operational.Waiting
. This means that if source[1].sig1()
occurs, then this will be handled by the Waiting
state,
not the Operational
state, reflecting the fact that inherit
dispatches based on the dynamic current state, not
the syntactic current state as that would be Operational
in the above.
To ensure that inherit
is well-defined, calls to setNextState
that would result in the
state where the await is active from being exited are prohibited. In the above example, that means that the state
Operational
is not allowed to be exited. This restriction is checked by the verification.
Await cannot be used inside entry
or exit
functions,
signal transitions
, or timer transitions
.
See also
Default Constructors¶
A default constructor expression evaluates to the default value of a type T that has an instance of
Default
declared for it.
The following example illustrates how a simple integer value can be reset using the default constructor for
Int
:
This simple function resetInt()
declares a local variable a
of type Int
with initial value 5
.
Variable a
is then reset using the default constructor, resulting in resetInt()
returning 0
.
Default constructors can also be used for more complex types such as arrays, for example:
In this example, resetArray()
declares a local variable store
as an array of integers of size 2, with
initial value [5, 5]
. The variable store
is then reset using the default constructor for the array type, resulting
in resetArray()
returning true
.
Cast¶
A cast expression converts an expression of one type into an expression of a different type.
-
expression
Cast
¶
Format Strings¶
A format string is an expression that converts a string containing expressions
into a value
of type String
. When a format string is evaluated, the expressions
are replaced by the string
representation of their values as given by the Log
trait, and then a single overall String
is built from the result.
-
expression
Format String
¶ $"«format string segments»" «format string segment» := {«expression»} | «string segment»
where a
string segment
consists of one or more UTF-8 encoded characters.
Format strings can contain arbitrary nested expressions. At runtime, the expressions are evaluated from left-to-right
and a string is built from the result. Coco uses the Log
trait to convert the value or the nested expression
into a string.
The following examples illustrate the use of format strings containing expressions:
For example, calling hello("Coco")
will return the following string:
"Hello, my name is Coco!"
Similarly, calling timingsToString([seconds(1), milliseconds(2)])
will return the following string:
"The timings for my experiment are [0:00:01,0:00:00.002000]."
The following example is of a format string containing an expression that calls another function:
Note
When typechecking, Coco will check that each nested expression is of a type that has an
instance of Log
. However, format strings
are not verified: this means
that errors inside log functions will not be caught. As a result of this, there is no instance of Log
on Slot<T>
.
Undefined¶
An undefined expression can be used when its type does not have an instance of Verified
.
Consider the following example:
external type ErrorCode
port P {
function begin() : Nil
outgoing signal ok()
outgoing signal failed(error : ErrorCode)
machine {
begin() = nondet {
ok(),
failed(_),
}
}
}
The argument passed to the signal failed(_)
is an undefined expression, which is valid because its parameter type
ErrorCode
is an external type, and therefore not verifiable.
The full form of an undefined expression (i.e. _ : T
) only needs to be used when there is ambiguity about which
overload of a function or signal would be called.
Side Effects¶
An expression is side-effect free precisely when it does not:
- Mutate any variables.
- Call any functions on a required port that are not labelled with the @noSideEffects attribute.
- Emit any signals in a provided port.
The arguments to the following built-in functions in Coco have to be side-effect free:
assert
, after
, and periodic
.
An expression is strictly side-effect free precisely when it is side-effect free, and it does not call any functions on
a required port. The following Coco constructs only permit expressions that are strictly side-effect free: state
invariants; and guards on transitions, nondet
clauses, and offer
clauses.
To illustrate this side-effect free property, consider the following examples of components below, which
use the following simple port P
:
port P {
@noSideEffects
function f1() : Bool
function f2() : Bool
machine {
// ...
}
}
P
declares two functions, f1()
and f2()
, where f1()
is labelled with the @noSideEffects
attribute.
The first example is of a component has P
as its provided port, but fails to comply with the requirement
that f1()
cannot have side-effects because it mutates the variable x
:
The second example is of a component that also fails to comply with the @noSideEffects
constraint
on f1()
, because it calls f2()
on its required port P
in response to a client.f1()
call event, and f2()
is not
declared as being side-effect free:
Some of the built-in functions in Coco, such as timer expiry events, also require their arguments to be side-effect free. The following component gives two examples of such events that violate this requirement:
The after
timer expiry event has an argument that modifies the variable d
, whereas the periodic
one results in an
assert
with a call to f2()
(which is not labelled as side-effect free) as its argument.
Note
The expression in a state invariant must be strictly side-effect free, whereas the expression given as an argument in
an assert
function that is not a state invariant must be side-effect free.
Statements¶
A statement describes some code that, unlike an expression, does not yield a value. They are terminated with a
semicolon (;
). An expression that ends with a semicolon may be used as a statement.
-
statement
Statement
¶
Return¶
-
statement
Return
¶ return «expression»;
Return statements are allowed anywhere within a function, including inside
while
loops and an if
expression.
The following example illustrates the use of return statements in two functions that have the same behaviour:
If the return type of a function interface
declaration is not verifiable, then the
corresponding return statement must have an undefined
expression,
as illustrated in the following example:
port P {
function getTimeOutPeriod() : Duration
machine {
getTimeOutPeriod() = {
return _;
}
}
}
Become¶
A become statement in Coco is a tail-call, which can be recursive. It can be used within a function
declaration.
-
statement
Become
¶ become «expression»;
where the expression must be a
function call
.
The following example is a function that computes factorials using tail recursion:
The use of a become statement is subject to the following restrictions:
- The called function and the calling function must have the same return type.
&out
parameters from the called function cannot be references to variables locally defined in the calling function.- The ordinal position of the output parameters within the argument list of the calling function must be same as their position within the argument list of the called function.
- An
&mut
parameter cannot modify the value of anin
parameter.
Loops¶
Loop statements allow blocks of code to be executed repeatedly, depending on the conditions
specified in the loop. Coco has two loop statements, namely a while
loop, and a
for
loop. Control flow in loops can be changed by using break
or
continue
statements.
While Loop¶
A while loop allows a block of code to be executed repeatedly until either the condition guarding the
while loop evaluates to false
, or the code within the while loop performs a
break
or continue
statement.
-
statement
While
¶ [«identifier»:] while («expression») «block expression»
where:
- The expression immediately after the keyword
while
is the condition that guards the while loop, and has to evaluate to a value of typeBool
. - The
block
expression is a block of code, which is executed on each iteration. - The optional identifier is a label on the while loop, which can be referenced by
break
andcontinue
statements.
- The expression immediately after the keyword
A while statement starts by evaluating its guard expression. If false
, then the
block expression is not executed. If the guard expression is true
, then the block expression will be
repeatedly executed until either the condition guarding the
while loop evaluates to false
, or the code within the while loop performs a
break
, continue
, or return
statement.
The following example is of a function that uses a while loop to compute the sum of integers between the values passed to the function:
For example, sequenceSum(1, 5)
would return 15
. In contrast, sequenceSum(1, 0)
would return 0
, since the guard
expression of the while loop immediately evaluates to false when executing the while statement.
See also
For Loop¶
A for loop statement iterates over a range of items in a collection, for example Array
,
and executes a block of code on each iteration.
-
statement
For
¶ [«identifier»:] for «identifier» [: «type»] in «expression» «block expression»
where:
- The first (optional) identifier is a label attached to the for loop, which can be referenced by
break
andcontinue
statements. - The identifier immediately following the keyword
for
is a locally declared variable, which is assigned the value of each item in the iterable expression. - This variable cannot be modified in the block expression of the for loop. The expression immediately
following the keyword
in
is an iterable expression, whose type must be a valid instance of theIterable
trait, for exampleArray<Int,10>
orrange(0,3)
. - The
block
expression is a block of code, which is executed on each iteration. It can have abreak
orcontinue
statement.
The following example illustrates the use of a for loop to iterate over a range of integers and return the sum:
In the following example, the for loop iterates over an array to compute the sum of the integers within it:
See also
Break and Continue¶
Coco has break and continue statements, which are used to change the control flow from
within loop expressions or a match
statement.
Break¶
A break statement ends program execution from within a for
or while
loop statement, or
match
expression.
-
statement
Break
¶ break [«identifier»];
where the identifier is a label defined on an enclosing
for
orwhile
loop, ormatch
expression.
When a break statement is used in a match
, the match is ended and gives the value nil
.
When used within a while
or for
loop, the execution of the
loop is ended. In the event that there are nested loops, then the innermost loop in which the break statement is
used is ended.
A break statement can also be used with a label on an enclosing for
, while
, or
match
. In this case, it will end the execution of the one with the matching
label. It is a static error if there is no enclosing loop statement, or match expression with a matching label.
The following example illustrates the use of a break statement without a label:
The following example rounds up the values stored in an array, and illustrates the use of labelled and unlabelled break statements in nested loops:
enum SimpleResult {
case OK
case BadData
case OverLimit
}
function roundArrayUp(target : Int, limit : Int, store : &mut Array<Int, size>) : SimpleResult = {
var result : SimpleResult;
outerLoop: for i in range(0, size) {
if (store[i] < 0) {
result = .BadData;
continue;
}
while (true) {
store[i] = store[i] + 1;
if (store[i] % target == 0) {
break;
}
if (store[i] >= limit) {
result = .OverLimit;
break outerLoop;
}
}
}
return result;
}
In this example, the first break statement will break out of the nested while
loop, but not the
for
loop. In contrast, the second break statement is labelled with outerLoop
, which refers to the outer
for
loop, thereby causing it to break out of the nested while
loop,
and the for
loop.
Continue¶
A continue statement ends program execution of the current iteration of a loop statement, but does not stop execution of the loop statement as a whole.
-
statement
Continue
¶ continue [«identifier»];
where the identifier is a label defined on an enclosing
for
orwhile
loop.
If a continue statement is made within nested loops, then only the innermost loop in which the continue statement is used is ended.
A continue statement can also be used with a label of an enclosing for
or while
loop.
In this case, it will end the execution of current iteration of the loop with the matching
label, without ending the overall loop statement itself. It is a static error if there is no enclosing loop
with a matching label.
The following example illustrates the use of a continue statement:
Simple Await¶
A simple await statement allows a component state machine to be blocked until a certain
event
occurs. A common use case is within the body of a function transition, where the state machine
has to wait for a specific signal before the function is allowed to return to its caller. Coco also supports
compound await
expressions, which is a variant of await allowing a state machine to be blocked on
multiple events, where the actions may differ in each case.
A simple await consists of an event
that it is waiting for,
and an expression that is evaluated when this event occurs. The event must either be a signal
receive event
, an empty event
, or an after
timer event
. A simple
await with an empty event
cannot be guarded.
A simple await statement can be used in a block
expression, as illustrated in the following example:
In this example, when the function begin()
is called on C
, it calls check()
on its required port source
,
waits for the signal result(v)
, and sends the signal done()
precisely when v
is true
. If this signal is not
guaranteed to be sent by source
, then C
will deadlock.
Await cannot be used inside entry
or exit
functions,
signal transitions
, or timer transitions
.
Note
Any variables declared in the parameters of an event are in scope within the block
it is declared in.
This differs from the scoping rules for compound await
expressions,
where any variables declared in the parameters of an event are only in scope within the resulting expression of the
corresponding await clause for that event.
See also
Visibility¶
Coco allows the visibility of declarations to be set in order to control which functions are accessible to other part of the code base. This helps to ensure safe use of public interfaces, and prevent code being inadvertently accessed.
-
syntax
Visibility
¶ «visibility» := private | protected | public(module) | public(package) | public
where:
private
: only visible within the containing context.protected
: only visible within the containing context, or in any subtypes of it. This is currently only relevant to ports as that is the only context where subtyping is relevant.public(module)
: declaration is visible within the current module only.public(package)
: declaration is visible within the current package only.public
: declaration is visible to code in any package.
When a declaration is marked with a specific visibility level, then that specifies how accessible the declaration is relative to its containing context (or scope). For example, a private declaration means that the declaration is private to its containing context, and thus can only be accessed by declarations within that context.
Coco modules can also be marked as private
, which means that the module is not accessible from another package.
This means that the module cannot be imported in any other package, and therefore the declarations within the
current module can only be used within the current package.
The default visibility of unmarked declarations
and modules is public
, with the
exception of state machines
, which are always protected
by default.
The following example illustrates how different levels of visibility can be specified within a struct
:
struct KeyValue<K, V> {
private var key : K
public var value : V
public static function create(key : K, value : V)
: KeyValue<K, V> = KeyValue<K, V>{ key = key, value = value }
}
In this example, the struct declaration called KeyValue
is public
by default. Within it, the variable
declaration for key
is marked as private
, making it inaccessible from outside of the struct.
In contrast, the remaining variable and function declarations are marked as public
.
There may be cases where private
is too restrictive, and yet public
is too permissive. For example, consider the case
where items of type KeyValue
need to be comparable for equality. This can be achieved either by declaring a custom
trait instance or using @derive(Eq)
. However, this would lead to an error
because the field declaration for key
is marked as private
, and is therefore invisible outside the declaration
of KeyValue
, even to the trait instance. An alternative is to
restrict the access of this field to be within a module only by using the public(module)
instead:
public(module) var key : K
This means that key
is visible to the corresponding trait instance providing it is in the same module.
The visibility of ports and components can also be customised. For example, the following external component is marked as
private
:
A common strategy for managing large projects is to declare each component and port in their own individual
module within a given package. The modules containing components and ports that should not be visible outside
of this package should be marked as private
, written as:
private module
This means that the declarations in this module are visible within the same package only. Within the remaining
modules (i.e. those not marked as private) in the package, any declarations that should not be visible to other packages should
be marked as such, for example, as private
or public(package)
. Consider the example of a
system that has the following top-level encapsulating component:
Using the strategy described above, this project is organised by having a Coco package that contains a module for each
of the components and ports within the traffic system, including one for the encapsulating component TrafficSystem
above.
This TrafficSystem
component is dependent upon the components and ports it needs to instantiate, which are located within the
same package. Now consider the case where other Coco packages are dependent upon the top-level component of this system only,
namely TrafficSystem
and its provided port TrafficControl
, but all other components and ports within the traffic
system package should be invisible outside of this package. This is achieved by marking
all the modules as private
, apart from those containing TrafficSystem
and TrafficControl
. In the event that either of
these modules contain declarations that should not be visible across packages, then they can be individually marked accordingly.
Events¶
Coco supports a range of events, which represent communications that can occur within or between components.
The most commonly used events are receive events
, which can either be function calls or signals, and
timer events
. There are also others to communicate the status of components, such as the
empty event
, which is used in constructs such as compound await
expressions.
-
event
Event
¶
The empty
event occurs when a component’s queue is empty.
-
event
Empty Event
¶ empty
For example, the empty event can be used in a simple await
to specify that a component state
machine is blocked until the component’s queue is empty.
The receive fallback
event matches any receive event
.
-
event
Receive Fallback Event
¶ _
See compound await
expressions for examples of how this event is used to catch
receive events
that are not matched by other clauses.
Patterns¶
A pattern represents the structure of a value, as opposed to the specific value of it.
A pattern can be matched with different values, and can be used to bind certain values to variables. They
are used in match
expressions, and event source
declarations.
-
pattern
Pattern
¶
Literal¶
-
pattern
Literal Pattern
¶
A literal pattern compares the value being matched against a literal
.
The following example is of a function that uses a literal pattern in a match
statement to
toggle the input value:
The value of x
will be compared to the integer literals 0
or 1
, and execute the corresponding clause that
matches.
Variable¶
-
pattern
Variable Pattern
¶
A variable pattern compares the value being matched against the value currently stored in the variable referred to by the dot identifier list.
The following function uses the variable pattern, and the wildcard
pattern to determine
whether the value of y
is equal to the square root of x
:
In this case, the value of the x * x
expression is compared with the value of the variable y
. If they are equal,
then the function returns true
, otherwise it returns false
.
Variable Declaration¶
-
pattern
Variable Declaration
¶ val «identifier» [: «type»]
A variable declaration pattern assigns the value being matched to the newly declared variable.
In the following example, the function uses a variable declaration pattern in the match
statement:
In this case, the variable declaration pattern val z
matches any value of the expression x * x
, and assigns
the value to the newly declared variable z
.
Enum Case¶
-
pattern
Enum Case
¶ «enum case name»[(«parameters»)] «enum case name» := «dot identifier list» | .«identifier» «parameter» := [var] «identifier» | _
where an enum case pattern cannot have an empty set of parentheses
()
. In other words, if an enum case pattern has parentheses, then it must have at least one parameter inside them.
An enum case pattern is used to match an enum value against a specific enum case, optionally binding the fields of the enum case to newly-declared variables.
The following example illustrates the use of an enum case pattern in a match
expression to extract values
in the fields of tagged enums
:
This example also illustrates the use of a guarded enum case
pattern in the third match clause, which
means that it only matches this pattern in the case where id < 3
evaluates to true
.
State¶
-
pattern
State
¶ [«state pattern».]«identifier»[(«parameters»)] «parameter» := [var] «identifier» | _
This matches a value of type State
for a particular state machine.
The following example illustrates the use of a state pattern in a match
expression within a
port state machine:
In this example, the function check(…)
takes an argument of type State
, and uses a match
expression to determine the return value of type Bool
. The third match clause is guarded by the expression
!topLevelOnly
, which evaluates to false
. This means that if check(B.X)
is called, then the only
pattern that matches is the wildcard
pattern, thereby returning false
.
Wildcard¶
-
pattern
Wildcard
¶ _
The wildcard pattern _
matches any value, and is commonly used as a catch-all clause.
Consider the following example:
This function returns true
precisely when the values of x
and y
are equal, and false
otherwise. The wildcard
pattern matches any value of x
, and is therefore used to catch all cases where the value of x
does not match the
value of y
in the first match clause.
Generics¶
Generic types allow the same Coco code to be used to perform the same operations on different types, while still ensuring type safety. This is a form of polymorphism.
In the simplest case, code only passes around, stores and retrieves values of a particular type. That is, it does not perform any operations on the values themselves. In this case, a generic version of the code can be written simply by adding annotations to indicate which types these are.
For example, consider the following identity functions:
Apart from having different types, they are essentially the same, as they just return whatever is passed to them. They could be replaced by a single generic function that works for any type:
function idGeneric<T>(x : T) : T = x
In a more complex case, where the code performs any operations on the values of the types in question, further information is needed to indicate how to perform these operations for each type of interest. This information is supplied by creating traits that group together the operations that every type must support and an instance for each type, which gives implementations of the operations. The code is then annotated with generic requirements to restrict it to types that are instances of the relevant classes.
Traits¶
Many types need to implement the same operations. For example, both booleans and integers can be compared for equality
via false == true
and 2 == 4
respectively.
Coco achieves this using traits. A trait is an interface with a type and constant parameters, which specify a set of functions that can be implemented in different ways, depending on the type parameters. For example:
Traits can be instantiated for different types using a trait instance. These have to specify the implementation of each of the trait’s functions:
instance Eq(Bool) {
external function equals(first : Bool, second : Bool) : Bool = ...
external function notEquals(first : Bool, second : Bool) : Bool = ...
}
instance Eq(Int) {
external function equals(first : Int, second : Int) : Bool = ...
external function notEquals(first : Int, second : Int) : Bool = ...
}
Coco has a number of builtin traits, for example: Default
, Eq
,
Ord
, and Verified
.
A description of the builtin traits in Coco can be found in the Traits section of the Standard Library.
Generic Declarations¶
Coco allows most declarations to be made generic, including:
Consider the following example of a generic function declaration:
This function takes a parameter x
of some type T
, and if the value of x
is equal to T
’s default value, then
it returns true
, otherwise it returns false
.
The notation <T where Default<T>, Eq<T>>
is called a generic signature, which restricts how a trait can be instantiated.
This generic signature specifies that isDefaultValue<T>
can only be instantiated with a type T
that is defaultable
and comparable for equality. This means that there is an instance of Default
and of Eq
declared for T
. Without this restriction, isDefaultValue<T>
could be instantiated for any type T
, even if it
does not make sense to do so.