Runtime verification in Erlang by using contracts

08/23/2018 ∙ by Lars-Åke Fredlund, et al. ∙ Universidad Politécnica de Madrid 0

During its lifetime, a program suffers several changes that seek to improve or to augment some parts of its functionality. However, these modifications usually also introduce errors that affect the already-working code. There are several approaches and tools that help to spot and find the source of these errors. However, most of these errors could be avoided beforehand by using some of the knowledge that the programmers had when they were writing the code. This is the idea behind the design-by-contract approach, where users can define contracts that can be checked during runtime. In this paper, we apply the principles of this approach to Erlang, enabling, in this way, a runtime verification system in this language. We define two types of contracts. One of them can be used in any Erlang program, while the second type is intended to be used only in concurrent programs. We provide the details of the implementation of both types of contracts. Moreover, we provide an extensive explanation of each contract as well as examples of their usage. All the ideas presented in this paper have been implemented in a contract-based runtime verification system named EDBC. Its source code is available at GitHub as an open-source and free project.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1 Introduction

Developing software is not an easy task and, consequently, some errors (bugs) can be introduced in the code. These errors can be really simple to solve but not so easy to find. Companies usually rely on program testing to check that their developed programs behave as expected. There are also programming languages that provide a robust static type systems which can unveil some errors during program compilation. These or similar methods are really useful to find the symptom of a bug. Once a symptom is found, the debugging process can start in order to find the source of the bug. The cause can be in a place that goes from the same line reported by the error to other modules. Most of the bug sources would be found easier and sooner if users defined further control in their code. For instance, consider a function which can perform a division by zero if one of its parameters has a non-expected value. Without any special control, the program would fail and report a division by zero if one of the non-expected values were used in the call. However, the bug source is in the call and not in the reported division operation. Defensive programming is a way to avoid non-expected values which produce unexpected behaviours, e.g. divisions by zero. The idea is to check the validity of the arguments before doing the real job. However, defensive programming is not a recomendable practice, for various reasons, like the boiler-plate or the overhead introduced in the final release. In fact, in our running language, Erlang, this practice is always banned in the good-programmer manuals.

Erlang innately has a lot of ways to control how to recover from errors, but these features are supposed to be used only for plausible errors. Unfortunatelly, not all the errors fall in this category. There are errors that simply should never happen, and they should be fixed during the software production. Erlang, as Python or JavaScript, is a dynamic programming language. This means that there is not a really strict control during compilation that could help avoiding errors before runtime. For this reason, static analysis techniques, led by Dialyzer [10], have been successfully and widely adopted by Erlang’s practitioners during the last years. Dialyzer can analyze the code and report some errors without any special input from users. However, its results can be considerably improved by the use of type contracts [8]. These contracts are not there to be only used by Dialyzer or similar tools, but also as a documentation that could improve the maintainability of the resulting software.

Dialyzer and testing, e.g. EUnit, are common Erlang tools used to find errors before the final software deployment. However, there are still some errors that can escape from the claws of these tools. Unfortunately, these errors can appear when the program is being used by the final user. In most of the cases, these errors have their source in a knowledge that users had when they were writing the code, e.g. that the result of a function must be greater than the first parameter. Unfortunately, programming languages rarely provide a method to input this information beyond defensive programming. In this work, we propose an alternative to reflect this knowledge: the Erlang Design-By-Contract (EDBC) system, a runtime verification system based on the Design-By-Contract [12] philosophy. The EDBC system is available as free and open-source software in GitHub: https://github.com/tamarit/edbc.

The Design-By-Contract approach is based on the definition of contracts which are checked for validity during runtime. Then, this approach allows users to add extra knowledge which can help to detect the bug source more easily and sooner. Unfortunately, contract checking usually comes with an overhead in the runtime. However, the contract checking is meant to be done only during software production, and it should be disabled when the final release is generated. Once disabled, the defined contracts can still be used as program documentation.

There are different types of contracts, most of them are related to program functions. The most common contracts are the preconditions and the postconditions. Preconditions are conditions that should hold before evaluating a function, while postconditions should hold after its evaluation. There are more types of contracts, e.g. the aforementioned type contracts (specs in Erlang). In concrete, our approach includes pre and postcondition contracts, type contracts, decreasing-argument contracts, execution-time contracts and purity contracts. All these contracts can be used in any Erlang program, i.e. with a sequential or concurrent evaluation. The descriptions of the contracts that our system propose are in Section 2.1. We also provide some examples of how the contracts are used and what are the result of using them. These examples can be found in Section 2.2. Implementation details are also presented in the paper. In concrete, the contract checking is performed by a program instrumentation (described in Section 2.3). There are a second type of contracts which only can be used in concurrent evaluations, in concrete, in programs using Erlang behaviours. The Erlang behaviours are formalizations of common programming patterns. The idea is to divide the code for a process in a generic part (a behaviour module) and a specific part (a callback module). Thus, the behaviour module is part of Erlang/OTP and the callback is implemented by the user. An example of an Erlang behaviour is the gen_server behaviour, that implements the the generic parts of a server. Therefore, a callback module implementing this behaviour needs to define the specific parts of a server, e.g. how the state is initialized (init/0), how to serve a request (synchronous handle_call/3 and asynchronous handle_cast/2), etc. Contracts for this Erlang behaviours are described in Section 3.1 while some examples of their usefulness can be found in Sections 3.2-3.3. Finally, we discuss where our approach is placed among similar approaches in Section 4 before concluding the paper in Section 5.

2 Contracts in Erlang

In this section we first introduce all the contracts available at the EDBC system (Section 2.1), then we show how they can be used to perform program verification (Section 2.2), and finally, we present some details of their implementation (Section 2.3).

    2.1 The contracts

  • Precondition contracts. With the macro ?PRE/1 we can define a precondition that a function should hold. The macro should be placed before the first clause of the contracted function. The single argument of this macro is a function without parameters, e.g. fun pre/0 or an anonymous function fun() -> end, that we call precondition function. A precondition function is a plain Erlang function. Its only particularity is that it includes references to the function parameters. In Erlang, a function can have more than one clause, so referring the parameter using the user-defined names can be confusing for both, for EDBC and also for the user. In order to avoid these problems, in EDBC we refer to the parameters by their position. Additionally, the parameters are rarely single variables and they can be more complex terms like a list or a tuple. We use the EDBC’s macro ?P/1 to refer to the parameters. The argument of this macro should be a number that ranges from 1 to the contracted function’s arity. For instance, ?P(2) refers to the second parameter of the function. A precondition function should return a boolean value which indicates whether the precondition of the contracted function is held or not. The precondition is checked during runtime before actually performing the call. Then, if the precondition function evaluates to false, the call is not preformed and a runtime error is raised.

  • Postcondition contracts. In a similar way than in preconditions, we can use the macro ?POST/1 to define a postcondition that a function should hold. The macro should be placed after the last clause of the contracted function. Its argument is again a function without parameters, that we call postcondition function. In this case, we need to refer to the result of the function, however Erlang has not any way to refer to it. Therefore, we use the EDBC’s macro ?R to this purpose. Additionally, we can also use ?P/1 macros to refer to the contracted function’s parameters within a postcondition function. The result of a postcondition function is also a boolean value. Therefore, a postcondition function is exactly the same as a precondition function with the only difference that a postcondition function can use the macro ?R in its body. Postcondition functions are checked right after the call is completely evaluated and a runtime error is raised if they evaluate to false.

  • Decreasing-argument contracts. These contracts are meant to be used in recursive functions and check that some of their arguments are always decreasing in each nested call. There are two types of macros to define these contracts: ?DECREASE/1 and ?SDECREASE/1. They both operate exactly in the same way with the exception that the ?SDECREASE/1 macro indicates that the argument should be strictly lower in each nested call, while the ?DECREASE/1 macro also allows the argument to be equal. The argument of both macros can be either a single ?P/1 macro or a list containing several ?P/1 macros. These contracts should be placed before the first clause of the function. Decreasing-argument contracts are checked each time a recursive function is found, by comparing the arguments of the current call with the nested call just before performing the actual recursive call. In case the contracted decreasing argument is not actually decreasing, a runtime error is raised and the call is not performed.

  • Execution-time contracts. EDBC introduces two macros that allow users to define contracts related with execution time: ?EXPECTED_TIME/1 and ?TIMEOUT/1. The macros should be placed before the first clause of the contracted function. The argument of these macros is a function without parameters called execution-time function. An execution-time function should evaluate to an integer which defines the expected execution time in milliseconds. Within the body of an execution-time function we can use ?P/1 macros to refer to the arguments. This feature is specially useful when dealing with lists or similar structures where the expected execution-time of the contracted function is related to their sizes. Both macros have a similar semantics, the only difference is that with macro ?EXPECTED_TIME/1 the EDBC system waits till the evaluation of the call finishes to check whether the call takes the expected time, while with macro ?TIMEOUT/1 EDBC raises an exception when the defined timeout is reached.

  • Purity contracts. This contract allows users to declare that certain functions do not perform any impure operation. This purity condition of a contracted function can be declared by using the macro ?PURE/0 before its first clause. The purity checking process is performed in two steps. First, before a call to a contracted function is performed, a tracing process is started. Then, once the evaluation of the contracted function call finishes and if a call to an impure function or operation has been found during its evaluation, a runtime error is raised. The way we check the purity of a function ensures no false positives neither negatives. The purity checking is not compatible with execution-time contracts which innately requiere to perform impure actions.

  • Invariant contracts. This contract is meant to be used in those Erlang behaviours with an internal and live state. An invariant contract is defined by using the macro ?INVARIANT/1. This macro can be placed anywhere inside the module implementing the behaviour. The argument of the ?INVARIANT/1 macro is a function, named invariant function, with only one parameter that represents the current state of the behaviour. Then, an invariant function should evaluate to a boolean value which indicates whether the given state satisfies the invariant or not. The invariant function is used to check the invariant contract each time a call to a state-modifier function , e.g. handle_call/3 from the gen_server behaviour, finishes. Invariant contracts together with an enhanced state, can be used to control some relevant question as starvation. Examples of invariant contracts are presented in Section 3.1.

  • Type contracts. Erlang has a dynamic type system, i.e. the types are not checked during compilation. However, we still can define type contracts (represented by spec attributes) which serves for both, to document the code and to help static analyzers like Dialyzer[10]. Therefore, they are considered a powerful tool to anticipate type errors palliating in this way the main inconvenience of the dynamic typing approach. These type contracts are not dynamically checked by the Erlang interpreter and, of course, it has sense because these checks would have a cost which is not desirable when the software is already released. However, when the software is still in production, checking these type contracts during runtime can be very helpful to uncover unexpected behaviours. For this reason, before a function is evaluated, EDBC checks the type contract of its parameters (if any), while its result is checked after its evaluation. When some of the expected types, for the parameters or for the results, do not correspond with the obtained value, a runtime error is raised. Note that EDBC does not use any special macro to check type contracts, spec attributes are used instead.

2.2 Verification of Erlang programs using contracts

1?PRE(fun() -> ?P(1) >= 0 end).
2?SDECREASES(?P(1)).
3fib(0) -> 0;
4fib(1) -> 1;
5fib(N) -> fib(N - 1) +  fib(N - 2).
Figure 1: Fibonacci numbers

In this section we show how to use the contracts presented in the previous section and also what are the benefits of using them.

We start by a simple example: the Fibonacci numbers. Figure 1 shows an implementation in Erlang with some contracts attached. In concrete, the contracts are defining that the parameter should be a non-negative integer and that it should be strictly decreased in each recursive call.

** exception error: {"Decreasing condition does
not hold. Previous call: fib(2).
Current call: fib(4).", [{ex,fib,1,[]},
Figure 2: Decrease contract violation

In case any of the contracts is found to be false, an error is raised. For instance, if we change fib(N - 2) in line 1 to fib(N + 2), the contract-violation error shown in Figure 2 is raised.

method Find(a: array<int>, key: int) returns (index: int)
   requires a != null
   ensures 0 <= index ==> index < a.Length && a[index] == key
   ensures index < 0 ==> forall k ::
      0 <= k < a.Length ==> a[k] != key
{}
Figure 3: Contracted function Find/2 in Dafny

We can use the EDBC system to define more advanced contracts. For instance, Dafny [9], which was an inspiration for our system, introduces the quantifiers as a way to define conditions for input lists. Figure 3 shows how these quantifiers are declared in Dafny for a function Find/2 which simply search for the position of a value in an array and return -1 if it is not found.

1?PRE(fun() -> length(?P(1)) > 0 end).
2find(L, K) -> 
3?POST(fun() -> ?R < 0 orelse
4        (?R < length(?P(1))
5         andalso lists:nth(?R, ?P(1)) == ?P(2)) end).
6?POST(fun() -> ?R > 0 orelse
7        lists:all(fun(K) -> K /= ?P(2) end, ?P(1)) end).
Figure 4: Contracted function find/2 in Erlang

These contracts with quantifiers can be also declared in Erlang using our approach. Instead of using a special syntax like in Dafny, we can check conditions with quantifiers using common Erlang function like lists:all/2 which checks whether a given predicate is true for all the elements of a given list. Figure 4 shows how the contracts in Figure 3 are implemented in Erlang. If we implemented this function as a recursive function the list would be decreasing between recursive calls. Then, we could also add the contract ?SDECREASE(?P(1)) to the function.

Figure 5: EDoc for the contracted function find/2

Note that two postconditions are defined for function find/2 in Figure 4. In this function it is pretty clear which one failed by observing the result, i.e. if it is negative or not, but it could happen that for other contracts it is not so clear. We provide an alternative output for the contract functions that allows to identify the failing contract more easily. Instead of defining a contract function that returns boolean values, users can also define a function that returns tuple containing a boolean value and as second element a message to be shown when the condition fails. This can be useful not only to distinguish among several contracts, but also to attach additional information which can help to easily identify the source of the unexpected behaviour. Additionally, users can add some logging data in all type of contracts. In this way, users can trace some relevant information during execution.

1fold1(Fun, Acc, Lst) -> lists:fold(Fun, Acc, Lst).
2fold2(Lst, Fun) -> fold1(Fun, 1, Lst).
3g3() -> fold1(fun erlang:put/2, ok, [computer, error]).
4?PURE.
5g4() -> fold2([2, 3, 7], fun erlang:’*’/2).
Figure 6: Example taken from PURITY [13]

Contracts added by users can also be used to generate documentation. Erlang OTP includes the tool EDoc [5] which generates documentation for modules in HTML format. We have modified the resulting HTML to add information of contracts. An example of an EDoc-generated documentation for function find/2 with information of its contracts (some in Figure 4 and some new) and its specs is depicted in Figure 5.

** exception error: {"The function is not pure.
Last call: ex:g3().
It has call the impure BIF erlang:put/2
when evaluating g3().",[{ex,g3,0,[]}]}
Figure 7: Pure contract-violation report

In order to show purity contracts we take a simple example used to present PURITY [13], an analysis that statically decides whether a function is pure or not. Although our goal here is different, it is still interesting to study what happens in the cases that they found more problematic. In concrete, its analysis loses some accuracy in cases where (in their own words) multiple levels of indirection are present between a call with a concrete function as argument and the actual higher order function which will end up using it.

1?EXPECTED_TIME(fun() ->
2    20 + lists:sum([case (I rem 2) of
3    0 -> 100; 1 -> 200 end|| I <- L]) end)
4f_time(L) -> [f_time_run(E) || E <- L].
5f_time_run(N) when (N rem 2) == 0 ->  timer:sleep(100);
6f_time_run(N) when (N rem 2) /= 0 -> timer:sleep(200).
Figure 8: Function with time contracts

The example the authors presented is depicted in Figure 6. We only added the contract ?PURE in the test case g4/0, because the other test case, i.e. g3/0, performs the impure operation erlang:put/2. When g4/0 is run, no contract violation is reported as expected. If we added the contract ?PURE to g3/0, then the execution will fail with the error shown in Figure 7.

** exception error: {"The execution of
ex:f_time2([1,2,3,4,5,6,7,8,9,10]) took too much time.
Real: 1509.913 ms. Expected: 1020 ms. Difference: 489.913 ms)
Figure 9: Execution-time contract-violation report

In order to understand how the time contracts work, we present an example of a function that performs a list of tasks. Each task has its type (even or odd), and the time is defined by this type (100 and 200 ms., respectively). Figure

8 shows this function and its associated time contract. In case we changed the execution-time function to something like fun() -> 20 + (length(?P(1)) * 100) end, we would obtain the contract-violation report shown in Figure 9.

** exception error: {"The spec precondition does not hold.
Last call: ex:fib(a).
The value a is not of type integer().", }
Figure 10: spec contract-violation report

For spec checking, users only have to define the function specs as usual or reuse the existing ones. Then, they run the code and an error will be raised only in case that some of the arguments or the result of a function call have an unexpected type. Figure 10 shows an error that would be shown in case of calling fib(a) when the spec fib(integer()) -> integer() is defined in function fib/1. Finally, we want to highlight that the contract checking performed by EDBC has not any incompatibility with other tools. For instance, users can define contracts and also EUnit [2] assertions in the same function. All these and more examples are collected at the GitHub repository111https://github.com/tamarit/edbc/tree/master/examples.

2.3 Implementation details

In this section we explain how we are instrumenting the code to get contract checking during runtime. Note that the code produced by the instrumentation process is normal Erlang code meant to be run in the standard interpreter of the language. Given a module, we run the algorithm in Figure 10(a) for each of its function definitions. The result of this algorithm is a collection of function definitions which contains the (maybe modified) input function definition and some helper functions which are synthetized according to the contracts. The instrumentation is performed in three steps.

1INPUT: A function definition 
2OUTPUT: A list of function definitions
3
4contracts = read_contracts()
5funs = 
6if  
7    (, funs) = inst_put_info(, funs, n) 
8if  
9    (, funs) = inst_decr(c, , , funs, n)
10    contracts = contracts \ {c}
11foreach 
12    if  type(c) = pre
13        (, funs) = inst_pre(c, , funs, n)
14    else
15        (, funs) = inst_post(c, , funs, n)
16    contracts = contracts \ {c}
17return 
(a) Instrumentation of a contracted function
1inst_decr(, , , funs, n) =
2     = get_free_name()
3     = get_name()
4     = { edbc_lib:decrease_check( 
5        ,
6        fun()   end).}
7     =
8         
9             
10            
11    return (, funs  )
(b) Instrumentation of a ?DECREASE contract
1inst_pre(, , funs, n) =
2     = get_free_name()
3     = get_name()
4     = { edbc_lib:pre(
5        fun()  replace() end,
6        fun()   end).}
7    return (rename_fun(, ), funs  )
(c) Instrumentation of a ?PRE contract
1inst_post(, , funs, n) =
2     = get_free_name()
3     = get_name()
4     = { edbc_lib:post(
5        fun() 
6            replace()
7        end,
8        fun()   end).}
9    return (rename_fun(, ), funs  nfuns)
(d) Instrumentation of a ?POST contract
1inst_put_info(, funs, n) =
2     = get_free_name() 
3     = get_name()
4     = { 
5        edbc_lib:put_info(.} 
6    return (rename_fun(, ), funs  nfuns) 
(e) Instrumentation to store information
Figure 11: Instrumentation functions for contracts
  1. First of all, if the function has any contract associated (Figure 10(a), line 10(a)), then a instrumentation to store information of the call (function name, arguments and stack trace) is performed (Figure 10(a), line 10(a)). This instrumentation (Figure 10(e)) creates a new function which becomes the entry point of the original function (Figure 10(e), line 10(e)). This new function first stores the needed information and then calls to the original function (Figure 10(e), line 10(e)) which has been renamed (Figure 10(e), line 10(e)) with a fresh name (Figure 10(e), line 10(e)). In function inst_put_info/3 and in the rest of instrumentation functions, variables fv represents fresh variables, function get_free_name/0 returns a fresh function name, function get_name/1 returns the function name of a given function definition, and function rename_fun/2 renames a function definition.

  2. Then, contracts of type ?DECREASES/1 (including ?SDECREASES/1) are processed (Figure 10(a), line 10(a)). This instrumentation (Figure 10(b)) creates a function which checks if the size of its parameters have decreased between recursive calls (Figure 10(b), line 10(b)). This new function () has two parameters, a list with the o arguments declared as decreasing of the previous call () and all the arguments of the next call (). Its body is a single call to the EDBC’s function decrease_check/3, which receives the value of the decreasing arguments in the previous call () and in the next call (222Symbol represents the selection of expressions in according to a given list of parameter numbers . For instance, is equal to .), and a delayed call to the contracted function’s entry point. Using this information, function decrease_check/3 compares previous and next values. If they are being decreased, it runs the delayed call, and if they are not, it raises a contract-violation error. During the instrumentation the original function is also modified by replacing all the recursive calls to calls to the new created function (Figure 10(b), lines 10(b)-10(b)). Function replace/2 applies a substitution to an expression or a list of expressions. Note that, due to this instrumentation and the previous one, we have changed the call cycle of a recursive call from to , where is the original function, is the function that stores the call information, and is the function that checks the decreasing of arguments.

  3. Finally, the rest of the contracts are processed distinguishing among those contracts of type ?PRE (Figure 10(c)) and those contracts of type ?POST (Figure 10(d)). All the contracts except ?DECREASE can be generalized to one of of these two types of contract. Of course, each contract has its particularities (explained below), however these particularities do not have any effect in the instrumentation process. Both instrumentations create a function whose body is a call to a EDBC’s function (pre/2 or post/2) that checks whether the pre- or the postcondition is held. This function receives two parameters: (1) the function which checks the conditions (named condition-checker function) replacing in its body the parameter/result macros by their correspondent variables, and (2) a delayed call that, in the case of ?PRE contracts, is run only in case the condition holds (a contract-violation is raised otherwise) or that, in the case of ?POST contracts, is run before contract checking and if the condition holds its result is returned (a contract-violation is raised otherwise). The original function is simply renamed like in the instrumentation which stores the call information. Therefore, when we call a function which defines pre- or a postcondition contracts, the chain of calls becomes , where are a number of functions (maybe none) introduced by ?PRE/?POST contracts. In the case of a recursive function which defines a ?DECREASE contract, the call chain would be . Note that most of the helper functions have a call as its last expression enabling, in this way, last call optimizations. In fact, the only exception is the functions generated for postconditions. These functions should be stacked until its internal call is completely evaluated.

In the rest of the section we explain the particularities of each contract type. Contracts of type ?DECREASE/1, ?PRE/1 and ?POST/1 have not any particularity, i.e. they work as explained in the instrumentation process. The rest of contract types are implemented as described in the following.

  • Execution-time contracts. These contracts are instrumented as ?PRE/1 contracts because the result, i.e. the parameter of the condition-checker functions of ?POST/1 contracts, is not needed. In fact, the evaluation of the condition-checker function does not return a boolean, but a number which represents the expected time. Then, the delayed call is run in the same process (?EXPECTED_TIME/1) or in a separate one (?TIMEOUT/1). Finally, according to the time needed to run the call, either the result is returned or a contract-violation error is raised (stopping also the evaluation in the case of ?TIMEOUT/1).

  • Purity contracts. They are also implemented as ?PRE/1 contracts for the same reason as execution-time contracts. In this case, there is not any condition-checker function, so a dummy one is used. In order to check these contracts, we trace all the calls performed during the evaluation of the delayed call as well as receive/send events. The tracing process is performed using the BIF erlang:trace/3. A function call is considered impure if during its evaluation is performed a send, a receive or a call to an impure BIF (checked using the function erl_bifs:is_pure/3).

  • Invariant contracts. These types of contracts are internally translated to ?POST/1 contracts and attached to functions which can change the state of an Erlang behaviour, e.g. code_change/3, handle_call/3, init/1, etc. Instead of the function result, the behaviour state is used to check whether the synthetized postcondition holds.

  • Type contracts. For checking the validity of the values according to the expected types we use the Sheriff [7] type checker. Sheriff is run by calling the function sheriff:check/2 which checks whether a given value belongs to a given type. We have gone a step forward making the type checking completely transparent for users and reusing their already-defined type contracts, i.e. their specs. A spec implicitly defines a condition for the parameters and a condition for the result. Therefore, a spec is translated to both a ?PRE/1 and a ?POST/1 contract which internally call to Sheriff and decide form its result whether to continue the evaluation or to raise a contract-violation error with attached details about the violator value and its expected type.

Finally, all these instrumentation processes have an effect in the total execution time that cannot be accepted in a final release. Therefore, after the code has been verified the code should return to its previous form, i.e. without instrumentation. For this reason, we have defined an easy mechanism to disable the instrumentation, e.g. with just a compilation flag.

3 Concurrent contracts

In this section, we explain the contracts that can only be used for concurrent evaluations. Although the contracts presented in Section 2 can still be really useful in this context, there are some scenarios where some special control is needed. This is where the contracts shown in this section shine. The section opens with the details of our approach and the contracts provided. The rest of the section proposes some scenarios where an implementation of these ideas can be applied to solve different kinds of problems.

3.1 gen_server with contracts

We have extended the gen_server behaviour by adding the callback function cpre/3 which allows users to decide whether the server is ready or not to serve a given request. The rest of the gen_server’s callbacks are not modified. The three parameters of function cpre/3 are 1) the request, 2) the from of the request333The from of the request has the same form than in the handle_call/3 callback, i.e. a tuple {Pid,Tag}, where Pid is the pid of the client and Tag is a unique tag. and 3) the current server state. The function cpre/3 should evaluate to a tuple with two elements. The first tuple’s element is a boolean value which indicates if the given request can be served. The second tuple’s element is the new server state which can be modified, e.g. by adding some control on the delayed requests.

The gen_server_cpre behaviour behaves exactly in the same way than the gen_server behaviour but with a particularity. Each time the server receives a synchronous request, it calls to cpre/3 callback before calling the actual gen_server callback, i.e. handle_call/3. Then, according to the value of the first element of the tuple that cpre/3 returns, either the request is actually performed (when the value is true) or it is queued to be served later (when the value is false). In both cases, the server state is updated with the value returned in the second element of the tuple. Thus, a cpre/3 callback can be seen as a kind of contract for concurrent environments.

EDBC includes two implementations of the gen_server_cpre behaviour, each one treats the queued requests in a different way. The simplest one is gen_server_cpre behaviour that resends to itself an unserveable request, i.e. a request for which function cpre/3 returns {false, …}). Since mailboxes in Erlang are FIFO, the posponed request will be the last request in the queue of pending requests. This can be considered as unfair because, once the server’s state is ready to serve the posponed request, it could serve requests that arrived later instead. Therefore, EDBC also provides a fairer version of the gen_server_cpre behaviour. In this version, three queues are used to control that older requests are served first: , , and . Each time the server is ready to listen for new requests, the is inspected. If it is empty, then the server proceeds as usual, i.e. by receiving a request from its mailbox. Otherwise, if it is not empty, a request from is served. Consequently, the taken request is removed from . The queues are also modified by adding requests to and . This is done when function cpre/3 returns {false, …}. Depending on the origin of the request it is added to (when it comes from ) or to (when it comes from the mailbox). Finally, each time a request is completely served, the server’s state could have been modified. A modification in the server’s state can enable posponed requests. Therefore, each time the server’s state is modified, is rebuilt as follows: .

3.2 Selective receives

We have found several posts where some limitations of the gen_server implementation are discussed. Most of them are related on how to implement a server which has the ability to delay some requests. One of theses examples is the following question asked in stackoverflow.com.

The questioner asked whether it was possible to implement a server which performs a selective receive while using a gen_server behaviour. None of the provided answers is giving an easy solution. Some of them suggest that the questioner should not use a gen_server for this, and directly implement a low-level selective receive. Other answers propose to use gen_server but delay the requests manually. This solution involves storing the request in the server state and returning a no_reply in the handle_call/3. Then, the request should be revised continually, until it can be served and use gen_server:reply/2 to inform the client of the result. Our solution is closer to the last one, but all the management of the delayed requests is completely transparent for the user.

1handle_call(test, _From, _State) ->
2  List = [0,1,2,3,4,5,6,7,8,9], 
3  lists:map(fun(N) -> spawn(fun() ->
4      gen_server:call(?MODULE, {result, N}) end)
5    end, lists:reverse(List)),
6  {reply, ok, List}; 
7handle_call({result, N}, _From, [N|R]) ->
8  io:format("result: " ++ integer_to_list(N) ++ "~n"),
9  {reply, ok, R}.
Figure 12: handle_call/2 for selective receive

Figure 12 shows the function handle_call/2 of the gen_server that the questioner provided to exemplify the problem. When the request test is served, it builds ten processes, each one performing a {result, N} request, with N ranging from 0 to 9. Additionally, the server state is defined as a list which also ranges from 0 to 9 (Figure 12, lines 12 and 12). The interesting part of the problem is how the {result, N} requests need to be served. The idea of the questioner is that the server should process the requests in the order defined by the state. For instance, the request {result, 0} can only be served when the head of the state’s list is also 0. However, there is a problem in this approach. The questioner explains it with the sentence: when none of the callback function clauses match a message, rather than putting the message back in the mailbox, it errors out. Although this is the normal and the expected behaviour of a gen_server, the questioner thinks that some easy alternative should exists. However, as explained above, the solutions proposed in the thread are not satisfactory enough.

1cpre(test, _, State) -> {true, State};
2cpre({result, N}, _, [N|R]) -> {true, [N|R]};
3cpre({result, N}, _, State) -> {false, State}.
Figure 13: cpre/3 for selective receive

With the enhanced versions of the gen_server behaviour we propose in this paper, users can define conditions for each request by using function cpre/3. Figure 13 depicts a definition of the function cpre/3 that solves the questioner’s problem without needing to redefine function handle_call/3 of Figure 12. The first clause indicates to the gen_server_cpre server that the request test can be served always. Contrarily, {result, N} requests only can be served when N coincides with the first element of the server’s state. Thus, with this simple code addition the questioner has a selective receive in a gen_server context.

3.3 Readers-writers example

1handle_call(request_read, _, State) ->
2  NState = State#state{readers = State#state.readers + 1},
3  {reply, pass, NState};
4handle_call(request_write, _, State) ->
5  NState =  State#state{writer = true}},
6  {reply, pass, NState}.
7
8handle_cast(finish_read, State) ->
9  NState = State#state{readers = State#state.readers - 1},
10  {noreply, NState};
11handle_cast(finish_write, State) ->
12  NState = State#state{writer = false},
13  {noreply, NState}.
Figure 14: Readers-writers request handlers

In this section we define a simple server that implements the readers-writers problem. We start introducing an implementation of the problem using the standard gen_server behaviour. The server’s state is a record defined as -record(state, readers = 0, writer = false). The requests that it can handle are four: request_read, request_write, finish_read and finish_write. The first two requests are synchronous (because clients need to wait for a confirmation) while the latter two are asynchronous (clients do not need confirmation). Figure 14 shows the handlers for these requests. They basically increase/decrease the counter readers or switch on/off the flag writer.

1?INVARIANT(fun invariant/1).
2
3invariant(#state{ readers = Readers, writer = Writer}) ->
4    is_integer(Readers) andalso Readers >= 0
5    andalso is_boolean(Writer)
6    andalso ((not Writer) orelse Readers == 0).
Figure 15: Readers-writers invariant definition

Having defined all these components, we can already run the readers-writer server. It will start serving requests successfully without any noticeable issue. However, the result in the shared resource would be a mess, mainly because we are forgetting a really important piece in this problem: its invariant, i.e. . We can define an invariant for the readers-writers server by using the macro ?INVARIANT/1 introduced in Section 2. Figure 15 shows how the macro is used and the helper function which actually checks the invariant. Apart from the standard invariant, i.e. (not Writer) orelse Readers == 0, the function also checks that the state’s field readers is a positive integer and that the state’s field writer is a boolean value.

If we rerun the server with the invariant defined, we obtain some feedback on whether the server is behaving as expected. In this case, the server is not a correct implementation of the problem. Therefore, an error should be raised due to the violation of the invariant. An example of these errors is shown in Figure 16. The error is indicating that the server’s state was {state,0,true} when the server processed a request_read which led to the new state {state,1,true} which clearly violates the defined invariant. The information provided by the error report can be improved by returning a tuple {false, Reason} in the invariant function, where Reason is a string to be shown in this contract-violation report after the generic message.

=ERROR REPORT====
** Generic server readers_writers terminating
** Last message in was request_read
** When Server state == {state,0,true}
** Reason for termination ==
** {{"The invariant does not hold.",
Last call: readers_writers:handle_call(
request_read, , {state,0,true}).
Result: {reply, pass,{state,1,true}}",
 [{readers_writers,handle_call,3,},
    ]}, }
Figure 16: Failing invariant report

In order to correctly implement this problem, we use function cpre/3 to control when a request can be served or not. Figure 17 shows a function cpre/3 which makes the server’s behaviour correct and avoids violations of the invariant. It enables request_read requests as long as the flag writer is switched off. Similarly, the request_write requests also require the flag writer to be switched off and also the counter readers to be 0. If we rerun now the server, no more errors due to invariant violations will be raised.

Although this implementation is already correct, it is unfair for writers as they have less chances to access the shared resource. We have several alternative implementations in the GitHub repository444https://github.com/tamarit/edbc/tree/master/examples/readers_writers, some of them are fairer than the solution presented above. All the solutions are implemented using the behaviour gen_server_cpre.

1cpre(request_read, _, State = #state{writer = false}) ->
2  {true, State};
3cpre(request_read, _, State) ->
4  {false, State};
5cpre(request_write, _,
6    State = #state{writer = false, readers = 0}) ->
7  {true, State};
8cpre(request_write, _, State) ->
9  {false, State}.
Figure 17: Readers-writers cpre/3 definition

The examples presented in Sections 3.2 and 3.3 are some of the examples available at the the GitHub repository555https://github.com/tamarit/edbc/tree/master/examples. In the examples directory there are implementations of semaphores, single-lane bridges and other classical problems in concurrency.

4 Related Work

Our contracts are similar to the ones defined in [1], where the function specifications are written in the same language, i.e. Curry, so they are executable. Being executable enables their usage as prototypes when the real implementation is not provided. Their contracts are functions in the source code instead of macros, so it is not clear whether they could be removed in the final release. There is not any mention about whether their contracts are integrated with a documentation tool like our contracts are with EDoc. Moreover, they only allow to define basic precondition and postcondition contracts, while we are providing alternative ones like purity or time contracts. Finally, our contracts for concurrent environments have a really different approach.

The work in [13] presents a static analysis which infers whether a function is pure or not. Since they are working in a static context and ours is dynamic, the purity checking is performed in completely different ways in each work. However, we can benefit from their results by, for instance, avoiding to execute functions that are already known to be impure, reporting earlier to the user a purity-contract violation. In the same way, our system can be used by them to check the validity of their statically-inferred results.

The type contract language for Erlang [8] allows to specify the intended behavior of functions. Their usage is twofold: 1) as a documentation in the source code which is also used to generate EDoc, and 2) to refine the analysis such as the one that performs Dialyzer. The contract language allows for singleton types, unions of existing types and the definition of new types. However, these types and function specifications do not guarantee type safety. This guarantee comes with Erlang which incorporates a strong typing that reports type errors during runtime. Although it is a powerful and useful analysis, strong typing usually detects an unexpected behaviour too far from its source. Therefore, when debugging a program, these type contracts can be really useful to report a type-contract violation earlier. This was the motivation to incorporate them in our system.

The contracts proposed for the concurrent environments follow the same philosophy as the specifications defined in [6, 15]. Indeed, our function cpre/3 takes its name from these works. Although these works were more focused in enabling the use of formal methods in order to verify nontrivial properties of realistic systems, in this paper we demonstrate that they can be also really useful for runtime verification and for the management of temporally-unservable requests.

Dafny [9] is a language which allows to define invariants and contracts in their programs. The main difference between their approach and ours is that their contracts are not checked during runtime, but during compile-time. Additionally, as we have explained in Section 2.2, we can replicate the same type of contracts in EDBC. However, our approach does not need an extra syntax or functionality to define complex contracts as Dafny does.

The aspect-oriented approach for Erlang (eAOP) presented in [3] shares some similarities with our work. eAOP allows the instrumentation of a program with a user-defined tracing protocol (at an expression level). This is able to report events to a monitor (asynchronous) as well as to force some part of the code to block waiting for information from the monitor (synchronicity). Our system could be used to a similar purpose but only at the function level. Additionally, thanks to the functionality freedom allowed in our contracts, our system enables the definition of synchronization operations at the user-defined contracts. More complex modifications of our system, such as the ones done in [11], can transform our work into a complete aspect-oriented system.

Also in Erlang, the work [4] defines a runtime monitoring tool which helps to detect such messages that do not match with a given specification. These specifications are defined through an automaton, which requires an extra knowledge from the user in both, the syntax of the specification and in the whole system operation. We propose a clear and easy way to define the conditions to accept or not a request without needing any special input from users.

Finally, JErlang [14] enables joins in Erlang. The authors reach their goal by increasing the syntax of the receive patterns so they could express matching of multiple subsequent messages. Although our goal and theirs are different, both approaches can simplify the way programmers solve similar kind of problems. Indeed, we could simulate joins by adding a forth parameter to the function cpre/3. This additional parameter would represent the unserved/pending requests. When the last request of a user-defined sequence (join) was received, the pending requests would be examined to check whether the required join could be served. A similar modification would be needed in handle_call/3 so the pending requests should be duly served by using gen_server:reply/2.

5 Conclusions

We have proposed different types of contracts for general Erlang which help programmers to define otherwise-lost knowledge of their programs’ expected behaviour. These contracts are meant to be checked during runtime, although its checking can be disabled for the final release. Additionally, automatic documentation can be generated from them. Our contracts use plain Erlang, without the need of defining new syntax or supporting libraries. We have also defined contracts for concurrent systems. These contracts solve scenarios like requests that should be served or not according to the server’s state. The simplicity of the approach and its integration in the Erlang behaviours ease the programming of systems using the proposed features.

There are multiple extensions of this work. For instance, by extending contracts to return a default value instead of an error when a contract is violated. A more general is to leave it up to the users how the system should react when a contract is violated. We also need a way to express errors, e.g. to define that we expect that when certain argument is wrong the called function returns an error. We could use tracing instead of transforming the program to enable recursive paths visiting more than one function. The decrease contracts can be also extended to receive the comparison function to be used when deciding that certain parameter is being decreased.

We can use our concurrent-contract approach to control starvation of systems. The idea is to use a mapping that goes from types of request to waiting requests, and that represents the fact that a delayed request is waiting for a concrete event to occur. The invariant can be used then to control starvation. Another extension consists in that the preconditions and postconditions used inside an Erlang behaviour could make the client of the resource to fail instead of the resource itself.

In a more general view, we can extend our system to automatically derivate EUnit or property tests from the contracts. In a similar way, contracts could be used in a property-based testing environment to define, for instance, that all the inputs of a function always hold a given property. With respect to invariants, we could also attach them to the function spawn in order to enable the definition of properties such as: the number of pending messages for this process cannot be greater than 1. Finally, we are trying to establish a relation between liquid types and our approach as we have found partial similarities.

References

  • [1] S. Antoy and M. Hanus.

    Contracts and specifications for functional logic programming.

    In C. V. Russo and N. Zhou, editors, Practical Aspects of Declarative Languages - 14th International Symposium, PADL 2012, Philadelphia, PA, USA, January 23-24, 2012. Proceedings, volume 7149 of Lecture Notes in Computer Science, pages 33–47. Springer, 2012.
  • [2] R. Carlsson and M. Rémond. EUnit: a lightweight unit testing framework for Erlang. In M. Feeley and P. W. Trinder, editors, Proceedings of the 2006 ACM SIGPLAN Workshop on Erlang, Portland, Oregon, USA, September 16, 2006, page 1. ACM, 2006.
  • [3] I. Cassar, A. Francalanza, L. Aceto, and A. Ingólfsdóttir. eAOP: an aspect oriented programming framework for Erlang. In N. Chechina and S. L. Fritchie, editors, Proceedings of the 16th ACM SIGPLAN International Workshop on Erlang, Oxford, United Kingdom, September 3-9, 2017, pages 20–30. ACM, 2017.
  • [4] C. Colombo, A. Francalanza, and R. Gatt. Elarva: A monitoring tool for Erlang. In S. Khurshid and K. Sen, editors, Runtime Verification - Second International Conference, RV 2011, San Francisco, CA, USA, September 27-30, 2011, Revised Selected Papers, volume 7186 of Lecture Notes in Computer Science, pages 370–374. Springer, 2011.
  • [5] Ericsson AB. EDoc. Available at: http://erlang.org/doc/apps/edoc/chapter.html, 2018.
  • [6] Á. Herranz-Nieva, J. Mariño, M. Carro, and J. J. Moreno-Navarro. Modeling concurrent systems with shared resources. In M. Alpuente, B. Cook, and C. Joubert, editors, Formal Methods for Industrial Critical Systems, 14th International Workshop, FMICS 2009, Eindhoven, The Netherlands, November 2-3, 2009. Proceedings, volume 5825 of Lecture Notes in Computer Science, pages 102–116. Springer, 2009.
  • [7] L. Hoguin and W. Dang. Sheriff. Available at: https://github.com/extend/sheriff, 2013.
  • [8] M. Jimenez, T. Lindahl, and K. Sagonas. A language for specifying type contracts in erlang and its interaction with success typings. In S. J. Thompson and L. Fredlund, editors, Proceedings of the 2007 ACM SIGPLAN Workshop on Erlang, Freiburg, Germany, October 5, 2007, pages 11–17. ACM, 2007.
  • [9] K. R. M. Leino. Dafny: An automatic program verifier for functional correctness. In E. M. Clarke and A. Voronkov, editors,

    Logic for Programming, Artificial Intelligence, and Reasoning - 16th International Conference, LPAR-16, Dakar, Senegal, April 25-May 1, 2010, Revised Selected Papers

    , volume 6355 of Lecture Notes in Computer Science, pages 348–370. Springer, 2010.
  • [10] T. Lindahl and K. Sagonas. Detecting Software Defects in Telecom Applications Through Lightweight Static Analysis: A War Story. In Programming Languages and Systems: Second Asian Symposium, APLAS 2004, Taipei, Taiwan, November 4-6, 2004. Proceedings, volume 3302 of Lecture Notes in Computer Science, pages 91–106. Springer, 2004.
  • [11] D. H. Lorenz and T. Skotiniotis. Extending Design by Contract for Aspect-Oriented Programming. CoRR, abs/cs/0501070, 2005.
  • [12] B. Meyer. Applying "Design by Contract". IEEE Computer, 25(10):40–51, 1992.
  • [13] M. Pitidis and K. Sagonas. Purity in Erlang. In J. Hage and M. T. Morazán, editors, Implementation and Application of Functional Languages - 22nd International Symposium, IFL 2010, Alphen aan den Rijn, The Netherlands, September 1-3, 2010, Revised Selected Papers, volume 6647 of Lecture Notes in Computer Science, pages 137–152. Springer, 2010.
  • [14] H. Plociniczak and S. Eisenbach. JErlang: Erlang with Joins. In D. Clarke and G. A. Agha, editors, Coordination Models and Languages, 12th International Conference, COORDINATION 2010, Amsterdam, The Netherlands, June 7-9, 2010. Proceedings, volume 6116 of Lecture Notes in Computer Science, pages 61–75. Springer, 2010.
  • [15] L. Åke Fredlund, J. Mariño, R. N. Alborodo, and Ángel Herranz. A testing-based approach to ensure the safety of shared resource concurrent systems. Proceedings of the Institution of Mechanical Engineers, Part O: Journal of Risk and Reliability, 230(5):457–472, 2016.