Skip to content

Commit

Permalink
modified: gumby/scenario.py
Browse files Browse the repository at this point in the history
Remove scenario language for_loop feature

for_loops code has been removed from scenario.py.
for_loops documentation and testscript  have been removed.
  • Loading branch information
pvanita committed Jan 30, 2022
1 parent ebf9a65 commit b043c55
Show file tree
Hide file tree
Showing 3 changed files with 5 additions and 416 deletions.
129 changes: 0 additions & 129 deletions docs/advanced_scenario_concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ Advanced Scenario Language Concepts
The document is meant to exemplify some of the more advanced features of the scenario language. Currently, these features refer to:

- Support for ``variables``
- Support for ``for`` loops

They will be exemplified and presented in further detail in the sections to follow:

Expand Down Expand Up @@ -106,131 +105,3 @@ In this example, we will have 20 active peers, 19 of which are querying each oth
The variable introduces a cleaner scenario. Moreover, if further changes are required to the key, one can simply change the value once, when the ``key`` is set. Previously, if the key needed to be changed, one would have to manually go through each of its occurrences and make the required modification.

For Loops
---------

Often in scenario files, it might be useful to have the same operation executed multiple times by a peer, or have a single operation executed once on multiple peers. Without ``for`` loops both of these use cases are possible, but would demand quite a considerable amount of coding effort, usually requiring that one repeatedly copy-pastes the same line multiple times, making the code messy and difficult to alter when changes are required.

This is the reason why ``for`` loops have been added to scenario files, as they facilitate the writing and maintenance effort of such experiments.

The current implementation of ``for`` loops supports the execution of a single experiment callback. The callback can be passed the control variable as a parameter, and similarly, the control variable can be used as the peerspec of the callback. The general structure of the ``for`` loop is the following:

``@<timestamp> for <control_variable> in <left_bound> to <right_bound> call <callable> [<unnamed_parameters>]* [<named_parameters>]* [{<peerspec>}]``

The following is an explanation of the elements of the above command line:

- ``<timestamp>`` refers to the time into the experiment when the for loop should be executed, and implicitly its associated experiment callback
- ``<control_variable>`` refers to an alias assigned to the control variable, by which it can be referenced in the future
- ``<left_bound>`` refers to the initial value that the ``<control_variable>`` will take. This bound is inclusive, and need not be lower than the ``<right_bound>``
- ``<right_bound>`` refers to the final value of the ``<control_variable>``. This bound is inclusive, and need not be higher than the ``<left_bound>``.
- ``<callable>`` refers the experiment callback
- <unnamed_parameters> and <named_parameters> refer to the unnamed and named parameters that the callback takes. The programmer can use the ``<control_variable>`` as a parameter to the ``<callable>``.
- ``<peerspec>`` defines which peers should execute the ``for`` loop.

The Control Variable
~~~~~~~~~~~~~~~~~~~~

The control variable can be seen as a regular variable which is visible only during the for loop. It is referenced using the same construct as normal variables:

``$<control_variable>``

The control variable can be used as a parameter of the experiment callback. It should be mentioned however that **if there already is a declared variable with the same name as the control variable, the ``for`` loop will still work, but the variable will take precedence over the control variable when it is used as a parameter**.

The Iteration Bounds
~~~~~~~~~~~~~~~~~~~~

The ``<left_bound>`` and ``<right_bound>`` are specified as integers, and are both inclusive. There is no mandatory relationship between the two bounds: they can be equal, or one of them can be greater than the other.

Intuitively, if the bounds are equal, then the ``for`` loop will iterate once, and the control variable will be equal to the bounds. If, however, the ``<left_bound> < <right_bound>``, the control variable will move in increments of 1 towards the ``<right_bound>``, starting from the ``<left_bound>``. If the ``<left_bound> > <right_bound>``, the control variable will move in decrements of 1 towards the ``<right_bound>``, again starting from the ``<left_bound>``.

The Peerspec
~~~~~~~~~~~~

Peerspec is short for *peer specification*, and generally speaking, it allows one to specify which peers should execute a callback (or on the opposite, which peers shouldn't execute it). **The ``for`` loop still allows a peerspec to be used, however, its functionality is limited**. The peerspec can only contain one element, which can be one of the following:

- A literal which identifies a peer by its ID. In this case, the chosen peer will execute all the ``for`` loop's iterations
- The control variable. In this case, each iteration will select a peer to execute the operation, granted there is a peer with an ID that is the same as the control variable at that iteration

As such, it is currently not possible to specify multiple literals, specify which peers should *not* execute the iterations, or specify a mixture of the aforementioned entities, together with the control variable in the peerspec of a ``for`` loop.

Examples
~~~~~~~~

Let us take a closer look at some examples, which demonstrate how ``for`` loops can be used, and what are some of their limitations.

The following is a simple example which shows a ``for`` loop where the control variable moves in increments of 1 from 1 to 10. Each peer executing the scenario will run the associated experiment callback (``do_some_work``) 10 times, since no peerspec is used to select which peers should it:

``@0:25 for i in 1 to 10 call do_some_work``

The following ``for`` loop is exactly the same bar the fact that the control variable will move in decrements of 1 from 10 to 1:

``@0:25 for i in 10 to 1 call do_some_work``

It should be mentioned that if we want we can make one, or both of the bounds negative - the ``for`` should still work as expected -:

``@0:25 for i in -5 to 1 call do_some_work``

If we wish we can also make the bounds equal. In such a situation, there would only be one iteration, where the control variable takes on the value of the bounds:

``@0:25 for i in 1 to 1 call do_some_work``

It is possible to use the control variable as an unnamed parameter, named parameter, or both, to the ``for`` loop's associated experiment callback. If we imagine that our ``do_some_work`` function has the following new definition: ``def do_some_work(self, a, b=None, c=None)``, then we could, easily use the ``for`` loop's control variable as one or more parameters to this method. Let us take a look at a possible combination:

``@0:25 for i in 1 to 10 call do_some_work $i b=100 c=$i``

In this example, parameters ``a`` and ``c`` will be assigned the value of ``i``, and ``b`` will be assigned the constant ``100``.

For loops can also have an associated peerspec. Currently, it is only possible to use either a literal or the control variable within it. An example using a literal might look like this:

``@0:25 for i in 1 to 10 call do_some_work {1}``

Here, the peer with ID ``1`` will execute the ``do_some_work`` experiment callback 10 times, while any other peer will ignore the ``for`` loop. Using the control variable instead would look like this:

``@0:25 for i in 1 to 10 call do_some_work {$i}``

In this case, each of the ``for`` loop iterations will each be executed by a different peer as identified by the control variable in the given iterations.

Special Use Cases
~~~~~~~~~~~~~~~~~

A known special case is when the control variable's name is the same as a variable's. **The ``for`` loop should still work, but the code's behavior might be slightly different if the control variable is used as a parameter**. Let us look at an example describing this case:

.. code-block:: none
@! set i foo
...
@10:00 for i in 1 to 100 call my_function $i {$i}
It might not be immediately obvious what the behavior of this ``for`` loop will be, so let us take a closer look. Initially, a variable named ``i`` is declared, and is assigned the value ``foo``. Later on in the code, a ``for`` loop is defined, having a control variable with the same name as a regular variable: ``i``. The ``for`` loop will call ``my_function`` 100 times, and it will pass ``i`` as a parameter. **In this case, the variable will take precedence over the control variable, hence, the value passed to ``my_function`` will be ``foo``**. **The control variable will take precedence over the variable in the peerspec, thus, it will filter out peers as described above**.

Invalid Use Cases
~~~~~~~~~~~~~~~~~

The following examples will not work due invalid syntax:

- ``@0:25 for i in 1 to 10 call my_function {$}`` - the peerspec may not contain the ``$`` without a variable name
- ``@0:25 for i 1 to 10 call my_function`` - invalid ``for`` loop structure
- ``@0:25 for i in 1 10 call my_function`` - invalid ``for`` loop structure
- ``@0:25 for i in 1 to call my_function`` - invalid ``for`` loop structure
- ``@0:25 for i in to 10 call my_function`` - invalid ``for`` loop structure

The following will not work due to other issues:

- Usage of a (non-control) variable in the peerspec:

.. code-block:: none
@! set j 1
...
@10:00 for i in 1 to 100 call my_function $i {$j}
- Peer negation in the ``for`` loop's peerspec:

``@10:00 for i in 1 to 100 call my_function $i {!3}``

- Multiple peer IDs in the ``for`` loop's peerspec:

``@10:00 for i in 1 to 100 call my_function $i {1,2,3,4}``
79 changes: 5 additions & 74 deletions gumby/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,47 +145,6 @@ def _parse_arguments(self, args):

return unnamed_args, named_args

def _parse_for_loop(self, loop):
"""
Parse a for loop, checking for its syntactical and lexical correctness and returning its functional components
:param loop: a string containing the for loop. The string should have the following structure:
'for <control_variable> in <lower_bound> to <higher_bound> call <callable> <optional_parameters>
<peer_specification>'. The value of the lower_bound needn't actually be lower than the higher
bound.
:return: return a tuple containing the following: the callable, the callable's argument line (unparsed),
the lower_bound, the higher_bound, an offset (either 1 of -1) indicating the for's index in/decrements,
the control variable (including the '$' character at the beginning)
"""
parts = loop.split(' ', 7)

# Less than 7 tokens means that we had a malformed 'for'
if len(parts) < 7:
raise Exception()

offset = 1
control_var, lo_bound, hi_bound, callable_function = parts[::2]
args = parts[7] if len(parts) == 8 else ''

# Check if the other tokens are correct, so as to avoid ambiguity
if ['in', 'to', 'call'] != [x.lower() for x in parts[1:6:2]]:
raise Exception()

# Convert the bound strings to integers
lo_bound = int(lo_bound)
hi_bound = int(hi_bound)

# control_var
control_var = '$' + control_var

# The lower and upper bounds are considered inclusive, hence the additional in/decrements
if lo_bound > hi_bound:
offset = -1
hi_bound -= 1
else:
hi_bound += 1

return callable_function, args, lo_bound, hi_bound, offset, control_var

def _parse_scenario_line(self, filename, line_number, line, peerspec):
"""
Expand Down Expand Up @@ -221,40 +180,12 @@ def _parse_scenario_line(self, filename, line_number, line, peerspec):

commands = []

if callable == 'for':
# If our current command is a 'for' loop, then we further parse the line
callable, args, lo_bound, hi_bound, offset, control_var = self._parse_for_loop(args)
unnamed_args, named_args = self._parse_arguments(args)

# get the indexes and keys of the control variable in the (un)named variables
control_index_args = [idx for idx in range(len(unnamed_args)) if unnamed_args[idx] == control_var]
control_index_named_args = [key for key in named_args if named_args[key] == control_var]
if self._re_substitution.match(peerspec):
# We have a substitution variable in the peerspec, which should be illegal in this branch
raise Exception()

for i in range(lo_bound, hi_bound, offset):
if not peerspec or (peerspec == control_var and i == self._peernumber) or \
(peerspec == str(self._peernumber)):

# Replace any arguments represented by the loop's control variable with its current value
str_i = str(i)
for idx in control_index_args:
unnamed_args[idx] = str_i
for key in control_index_named_args:
named_args[key] = str_i

# We need to copy the parameter list and dictionary since there's reference issues otherwise
commands.append((begin, filename, line_number, callable, unnamed_args[:], dict(named_args)))
else:
# TODO: a regex should be added to swap in the value of a variable used in the peerspec;
# one can check if the variable exists, and identifies this peer in _parse_for_this_peer
# the aforementioned function can also be the place where the variable is swapped
# if the command is a for, then the variable should be swapped only if it's not the loop's
# control variable
if self._re_substitution.match(peerspec):
# We have a substitution variable in the peerspec, which should be illegal in this branch
raise Exception()

unnamed_args, named_args = self._parse_arguments(args)
commands = [(begin, filename, line_number, callable, unnamed_args, named_args)]
unnamed_args, named_args = self._parse_arguments(args)
commands = [(begin, filename, line_number, callable, unnamed_args, named_args)]

return commands

Expand Down
Loading

0 comments on commit b043c55

Please sign in to comment.