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

typeType

Type Reference

A type reference refers to an existing non-generic type declaration or constant.

typeType Reference

Consider the following example of type reference representations:

val size : Int = 4
var result : Array<Bool, size>

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.

typeInstantiated

where there must be at least one type specified in the list of types between the angle brackets.

Consider the following variable declarations:

    var a : Array<Bool, 3>
    var b : Bounded<0, 4>
    var c : Slot<Int>
    var d : NonVerified<Int>

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.

typeReference
[& | &mut | &out] «type»

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.

typeFunction

Function types are not comparable for equality, and are not defaultable, but they are verifiable.

Consider the following example:

(Int, Int) -> Bool

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 isEqual(a : Int, b : Int) : Bool = a == b
function notEqual(a : Int, b : Int) : Bool = a != b

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:

var x : (Int, Int) -> Bool = isEqual

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:

function square(a : Int, b : &out Int) : Nil = {
  b = a * a;
}

In this case, the function type for square is:

(Int, &out Int) -> Nil

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

Functions

Binary Type

A binary type refers to a type that is defined using a binary operator.

typeBinary
«type» «operator» «type»

Consider the following example of a binary type, which uses the addition operator:

val kLimit : Int = 7
var counter : Bounded<0, kLimit + 1>

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.

typeUnary
«operator» «type»

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.

Dependent Member Type

typeDependent Member

where there must be at least one type specified in the list of types between the angle brackets.

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.

declarationField

where the expression is a simple expression, and can only be used as a field initialiser in components.

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.

declarationVariable

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:

var x : Int
val y : Bool = true

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:

val x : Int = y
val y : Int = 5

Instead, the valid ordering in Coco is:

val y : Int = 5
val x : Int = y

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:

var z : Slot<Int>

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.

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.

declarationStatic Constant

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:

enum Style {
  case Painted(red : Int, green : Int, blue : Int)
  case Oak
}

struct Table {
  static val kBase : Style = .Oak
  var size : Int
  var tableTop : Style
}

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.

declarationFunction
[«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:

function id(argument : Int) : Int = {
  return argument;
}

function id2(argument : Int) : Int = argument

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:

function square(x : Int) : Int = x * x

function testSquare(x : Int, y : &out Int) : Nil = {
  var v : Int;
  y = square(x);
  v = x + 1;
  y = square(v);
}

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:

function f1(x : &out Optional<Int>) : Nil = {
  x.reset();
  x = Optional.Some(5);
}

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:

function f2(x : &out Optional<Int>) : Bool = {
  var y : Bool;
  y = x.hasValue();
  return y;
}

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:

function f3(x : &out Optional<Int>) : Bool = {
  var y : Bool;
  x = Optional.Some(5);
  y = x.hasValue();
  return y;
}

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:

function square(x : Int) : Int = x * x

function testSquare2(var x : Int, y : &out Int) : Nil = {
  y = square(x);
  x = x + 1;
  y = square(x);
}

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:

function InvalidFn(x : Int, y : &out Int) : Nil = {
  var z : Int = y;
}

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:

function increment(x : Int, y : &out Int) : Nil = {
  y = x + 1;
}

function testIncrement() : Nil = {
  var z : Int = 1;
  increment(z, &z);
  assert(z == 2);
}

& 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:

function f(x : &out Bool) : Nil = g(x)

function g(x : &out Bool) : Nil = {
  x = true;
}

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:

function compute(x : Int, y : (Int) -> Int) : Int = y(x)

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:

function square(x : Int) : Int = x * x

function cube(x : Int) : Int = x * x * x

function testCompute(a : Int) : Int = {
  val x : Int = compute(a, cube);
  val y : Int = compute(a, square);
  return x + y;
}

The following example illustrates how a function can take functions as arguments, and have a function type as its return type:

function f1() : (&out Bool) -> Nil = f2

function f2(b : &out Bool) : Nil = {
  b = true;
}

function f3() : Bool = {
  val vF1 : () -> (&out Bool) -> Nil = f1;
  val vF2 : (&out Bool) -> Nil = vF1();

  var b : Bool;
  vF2(&b);
  return b;
}

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.

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.

declarationFunction Interface
[«visibility»] function «identifier»(«parameters») : «type»

«parameter» := «identifier» : «type»

For example:

function begin() : Nil
function check(v : Int) : Bool

Signals

A signal declaration is used to declare a signal in a port declaration.

declarationSignal
[«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.

declarationEnum
[«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:

@derive(Default, Eq)
enum Colour {
  case RGB(red : Int, green : Int, blue : Int)
  case None
}

function isEqualColour() : Bool = {
  val v : Colour;
  return v == .RGB(0, 0, 0);
}

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:

@derive(Default)
enum ErrorStatus {
  case None
  case Error(id : Int)

  mut function reset() : Nil = {
    self = None;
  }

  function status() : Bool = {
    match (self) {
      None => false,
      Error(_) => true,
    }
  }
}

The following simple functions illustrate how a variable can be declared to be of type ErrorStatus, and its member functions called:

function hasError1() : Bool = {
  var error : ErrorStatus;
  error = .Error(3);
  return error.status();
}

function hasError2() : Bool = {
  var error : ErrorStatus;
  error = .Error(3);
  error.reset();
  return error.status();
}

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

@derive

Structures

A structure, also called a struct, combines several types into a larger compound type, giving each value within the structure a name.

declarationStruct
[«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:

function isEqual(x : Message, y : Message) : Result<Nil, Message> =
  if (x == y) {
    return .Ok(nil);
  } else {
    return .Error(y);
  }

function run(x : Message, y : Message, z : Message) : Result<Bool, Message> = {
  isEqual(x, y)?;
  isEqual(x, z)?;
  return .Ok(true);
}

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.

declarationType Alias

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.

declarationExternal 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:

function convertToBool() : Bool = {
  var command : Bool;

  command = match (getCommand()) {
    .Start => false,
    .Stop => true,
  };
  return command;
}

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:

external function writeBool(v : &out Bool) : Nil = {
  v = nondet {
    true,
    false,
  };
}

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:

external function getTimeoutValue(v : &out NonVerified<Int>) : Nil = {
  v = _;
}

function getDuration() : Duration = {
  var timeout : Slot<Int>;
  getTimeoutValue(&timeout);
  return seconds(timeout);
}

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.

declarationExternal 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:

@derive(Default)
external type Data

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:

@derive(Default)
external type Vector<T, val max : Int where 0 <= max> {
  var size_ : Bounded<0, max>

  mut external function clear() : Nil = {
    size_ = 0;
  }

  external function size() : Bounded<0, max> = size_

  mut external function pushBack(x : T) : Nil = {
    size_ = size_ + 1;
  }
}

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:

function testVector() : Nil = {
  var store : Vector<Bool, 5>;
  assert(0 == store.size());
  store.pushBack(false);
  store.pushBack(false);
  assert(2 == store.size());
  store.clear();
  assert(0 == store.size());
}

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:

@CPP.mapToType("std::vector<{0}>", .ConstReference, .System("vector"))
external type Vector<T, val max : Int where 0 <= max> {

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.

declarationExternal Constant

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.

expressionExpression

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.

expressionVariable Reference

Member Reference

A member reference expression is used to reference the value of a field within an instance of some type.

expressionMember Reference

For example, consider the following struct declaration:

struct Date {
  var day : Int
  var month : Int
  var year : Int
}

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.

expressionImplicit 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:

Struct Literal

An struct literal expression is used to assign values to fields in a struct.

expressionStruct 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.

expressionBinary Operator

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.

expressionUnary 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.

expressionTry Operator

where the expression must evaluate to a value of type Result or Optional.

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>:

function check(x : Int) : Result<Int, Nil> =
  match (x) {
    0 => .Error(nil),
    _ => .Ok(x),
  }

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:

function sum(x : Int, y : Int, z : &mut Int) : Result<Bool, Nil> = {
  z = check(x)? +check(y)?;
  return .Ok(true);
}

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:

function sum2(x : Int, y : Int, z : &mut Int) : Result<Bool, Nil> = {
  check(x)?;
  check(y)?;
  z = x + y;
  return .Ok(true);
}

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:

function step(x : Int) : Optional<Int> =
  match (x) {
    0 => .None,
    _ => .Some(x),
  }

function multipleSteps(x : Int, y : Int, z : Int) : Optional<Int> = {
  step(x)?;
  step(y)?;
  step(z)?;
  return .Some(x + y + z);
}

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.

See also

Result

Optional

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.

expressionBlock

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:

function quad1(x : Int) : Int = {
  var y = x + x;
  y = y + y;
  y
}

In contrast, the following function is an example of one with a block expression that does not end with an expression:

function ignore(x : Int) : Nil = {
  var y = x + x;
  y = y + y;
  y;
}

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:

function quad2(x : Int) : Int = {
  var y = x + x;
  y = y + y;
  return y;
}

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.

expressionAssignment

The value of a mutable variable can be modified using an assignment expression, for example:

function modifyVar() : Nil = {
  var x : Int = 5;
  x = x + 1;
  assert(x == 6);
}

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:

  var x : Array<Bool, 2>;
  var y : Array<Int, 4>;

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.

expressionArray Literal

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:

  var a : Array<Int, 4> = [2, 4, 6, 8];
expressionArray Subscript

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:

  var m : Array<Array<Bool, 2>, 3>;

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.

See also

Array

SymmetricArray

SymmetricArrayIndex

Arrays in event source declarations

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.

expressionFunction 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):

function sum(x : Int, y : Int) : Int = x + y

function testSum() : Nil = {
  val x : Int = sum(2, 3);
  assert(x == 5);
}

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.

expressionSignal 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.

expressionIf

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:

function toggle1(x : Int) : Int = {
  if (x == 0) {
    return 1;
  } else {
    return 0;
  }
}

function toggle2(x : Int) : Int = {
  if (x == 0) {
    return 1;
  }
  return 0;
}

function toggle3(x : Int) : Int = if (x == 0) 1 else 0

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.

expressionMatch
[«identifier»] match («expression») {
  «match clauses»
}

«match clause» := «pattern» [if («expression»)] => «expression»,

where the match expression must evaluate to a value of type Bool, Int or enum. The expression in the optional guard must be a simple expression, and must evaluate to a value of type Bool.

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:

function safeToggle(x : Int) : Int = {
  match (x) {
    0 => 1,
    1 => 0,
    _ => x,
  }
}

function squareUnlessRootOf(x : Int, y : Int) : Int = {
  match (x * x) {
    y => x,
    val z => z,
  }
}

The following example illustrates how values of a variable within an enum can be extracted in a match expression:

val Max : Int = 5

enum Message {
  case None
  case Data(msg : Bounded<0, Max - 1>)
  case DataWithId(id : Int, msg : Int)
}

function unpackMessage(m : Message) : Int = {
  match (m) {
    .None => -1,
    .Data(d) => d,
    .DataWithId(id, d) if (id < 3) => d,
    _ => -1,
  }
}

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:

function f1(y : Int, z : &out Int) : Nil = {
  L1: match (y) {
    0 => {
      z = 5;
    },
    _ => {
      L2: match (y * 2) {
        10 => {
          z = 15;
        },
        20 => {
          z = 30;
          break;
        },
        30 => {
          z = 40;
          break L2;
        },
        _ => {
          z = 0;
          break L1;
        },
      };
      break;
    },
  };
}

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.

See also

See the Patterns section for further examples of the different kinds of patterns that can be used in a match expression.

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.

expressionNondet
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 type Bool, and is strictly side-effect free. If the expression in the nondet clause is a block 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. {}).

expressionOptional
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.

expressionCompound 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:

  machine {
    client.begin() = {
      var result : Array<Bool, 2>;

      source[0].begin();
      source[1].begin();

      await (result[0] && result[1]) {
        source[val i].isDone(status : Bool) => {
          result[i] = status;
        },
      };
      client.ready();
    }
  }

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.

expressionInherit
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

Simple await

Default Constructors

A default constructor expression evaluates to the default value of a type T that has an instance of Default declared for it.

expressionDefault Constructor

The following example illustrates how a simple integer value can be reset using the default constructor for Int:

function resetInt() : Int = {
  var a : Int = 5;
  a = Int();
  return a;
}

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:

function resetArray() : Bool = {
  var store : Array<Int, 2> = [5, 5];
  store = Array<Int, 2>();
  return (store[0] == 0 && store[1] == 0);
}

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.

expressionCast

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.

expressionFormat 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:

function hello(name : String) : String = $"Hello, my name is {name}!"

function timingsToString(t : Array<Duration, 2>) : String =
  $"The timings for my experiment are {t}."

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:

function sum(results : Array<Int, 5>) : Int = {
  var total : Int;
  for index in results {
    total = total + index;
  }
  total
}

function scoreToString(results : Array<Int, 5>) : String =
  $"My total score is {sum(results)}."

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>.

See also

log function.

Logging Events.

Log trait, which also includes examples of using the @derive(Log) attribute on Coco-defined types, such as enums and structs.

Undefined

An undefined expression can be used when its type does not have an instance of Verified.

expressionUndefined
_ [: «type»]

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:

@runtime(.MultiThreaded)
component InvalidC1 {
  val client : Provided<P>

  machine {
    var x : Int
    client.f1() = {
      x = 1;
      true
    }
  }
}

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:

@runtime(.MultiThreaded)
component InvalidC2 {
  val client : Provided<P>
  val server : Required<P>

  machine {
    client.f1() = server.f2()
  }
}

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:

@runtime(.MultiThreaded)
component InvalidC3 {
  val client : Provided<P>
  val server : Required<P>

  machine {
    var n : Int
    var d : Duration

    state S0 {
      after((d = milliseconds(20))) = abort()
      periodic(milliseconds(20)) = assert(server.f2())
    }
  }
}

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.

statementStatement

Declaration Statement

statementDeclaration

Return

statementReturn

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:

function greater3(x : Int, y : Int) : Bool = {
  return x > y;
}

function greater4(x : Int, y : Int) : Bool = {
  if (x > y) {
    return true;
  } else {
    return false;
  }
}

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.

statementBecome

where the expression must be a function call.

The following example is a function that computes factorials using tail recursion:

function fact2(x : Int, tmp : Int) : Int = {
  if (x == 1) tmp else {
    become fact2(x - 1, tmp * x);
  }
}

The use of a become statement is subject to the following restrictions:

  1. The called function and the calling function must have the same return type.
  2. &out parameters from the called function cannot be references to variables locally defined in the calling function.
  3. 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.
  4. An &mut parameter cannot modify the value of an in 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.

statementWhile

where:

  • The expression immediately after the keyword while is the condition that guards the while loop, and has to evaluate to a value of type Bool.
  • 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 and continue statements.

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:

function sequenceSum(start : Int, end : Int) : Int = {
  var count : Int = start;
  var sum : Int = 0;
  while (count <= end) {
    sum = sum + count;
    count = count + 1;
  }
  return sum;
}

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.

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.

statementFor

where:

  • The first (optional) identifier is a label attached to the for loop, which can be referenced by break and continue 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 the Iterable trait, for example Array<Int,10> or range(0,3).
  • The block expression is a block of code, which is executed on each iteration. It can have a break or continue statement.

The following example illustrates the use of a for loop to iterate over a range of integers and return the sum:

function sequenceSum2(start : Int, end : Int) : Int = {
  var sum : Int = 0;
  for count in range(start, end) {
    sum = sum + count;
  }
  return sum;
}

In the following example, the for loop iterates over an array to compute the sum of the integers within it:

function sumArray(numbers : Array<Int, 4>) : Int = {
  var sum : Int = 0;
  for num in numbers {
    sum = sum + num;
  }
  return sum;
}

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.

statementBreak
break [«identifier»];

where the identifier is a label defined on an enclosing for or while loop, or match 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:

function roundUp(x : Int) : Int = {
  var y : Int = x;

  while (true) {
    y = y + 1;
    if (y % 10 == 0) {
      break;
    }
  }
  return y;
}

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.

statementContinue
continue [«identifier»];

where the identifier is a label defined on an enclosing for or while 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:

function oddSequenceSum(start : Int, end : Int) : Int = {
  var sum : Int = 0;

  for count : Int in range(start, end) {
    if (count % 2 == 0) {
      continue;
    }
    sum = sum + count;
  }
  return sum;
}

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.

statementSimple Await
Standard:1.1
await «event»;

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:

@runtime(Runtime.MultiThreaded)
@queue(1)
component C {
  val client : Provided<P>
  val source : Required<R>

  machine {
    client.begin() = {
      source.check();
      await source.result(v);
      if (v) client.done();
    }
  }
}

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

Compound await

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.

syntaxVisibility
«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:

@runtime(.MultiThreaded)
private external component BarrierControlBase {
  val client : Provided<BarrierControl>
}

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:

@runtime(.MultiThreaded)
component TrafficSystem {
  val client : Provided<TrafficControl>
  val barriers : BarrierControlBase
  val lights : LightSystem

  init() = {  }
}

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.

eventEvent

The empty event occurs when a component’s queue is empty.

eventEmpty 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.

eventReceive 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.

patternPattern

Literal

patternLiteral 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:

function toggle(x : Bounded<0, 1>) : Bounded<0, 1> = {
  match (x) {
    1 => 0,
    0 => 1,
  }
}

The value of x will be compared to the integer literals 0 or 1, and execute the corresponding clause that matches.

Variable

patternVariable 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:

function isSquareRoot(x : Int, y : Int) : Bool = {
  match (x * x) {
    y => true,
    _ => false,
  }
}

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

patternVariable Declaration

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:

function giveSquare(x : Int) : Int = {
  match (x * x) {
    val z => z,
  }
}

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

patternEnum 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:

val Max : Int = 5

enum Message {
  case None
  case Data(msg : Bounded<0, Max - 1>)
  case DataWithId(id : Int, msg : Int)
}

function unpackMessage(m : Message) : Int = {
  match (m) {
    .None => -1,
    .Data(d) => d,
    .DataWithId(id, d) if (id < 3) => d,
    _ => -1,
  }
}

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

patternState
[«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:

port P {
  machine {
    val topLevelOnly : Bool = true

    function check(s : State) : Bool = {
      match (s) {
        A => true,
        B => true,
        B.X if (!topLevelOnly) => true,
        _ => false,
      }
    }

    state A {}
    state B {
      state X {}
    }
  }
}

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

patternWildcard
_

The wildcard pattern _ matches any value, and is commonly used as a catch-all clause.

Consider the following example:

function isEqual(x : Int, y : Int) : Bool = {
  match (x) {
    y => true,
    _ => false,
  }
}

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:

function idInt(x : Int) : Int = x
function idBool(x : Bool) : Bool = x

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:

trait Eq<T> {
  function equals(first : T, second : T) : Bool
  function notEquals(first : T, second : T) : Bool
}

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:

  • Function — to write a function that takes arguments of any type, or any type that is an instance of a certain trait;
  • Enum — to build a parameterised compound type;
  • Struct — to build a parameterised compound type;
  • Type — to declare a parameterised alias for a parameterised type or external type;

Consider the following example of a generic function declaration:

function isDefaultValue<T where Default<T>, Eq<T>>(x : T) : Bool = x == T()

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.