Unverified Commit 647342a4 authored by Maxime Perrotin's avatar Maxime Perrotin Committed by GitHub

Merge pull request #79 from wbnns/wbnns-design-documentation

wiki: Add Design Documentation
parents eab0ad4d a35f9352
= OpenGEODE Design documentation =
== Introduction ==
This document describes some parts of the design of the tool, as well as
practical guidelines to implement new features.
The first chapter explains the steps to be followed in order to implement SDL
language features that are not yet supported by the tool and is based on a
concrete use case: adding support for ''Continuous signals''.
The source of this document is in the ''Markdown'' format and other formats are
generated using the ''pandoc'' tool. Please take it in consideration when
updating the text.
== Guideline for extending OpenGEODE with Continuous Signals ==
Continuous signals are part of the SDL language and their semantics and syntax
is formally specified in the Z100 standard.
This chapter explains how concretely to add support for this construct to the
tool. It covers:
* Extending the ANTLR grammar
* Updating the parser and the AST (metamodel), including syntax/semantic checks
* Creating a new graphical symbol and updating the renderer and the menus
* Updating the backend to parse the graphical model and save PR files
* Updating other backends such as the Statechart renderer and code generators
* Updating the clipboard functionality
This is explained step by step in an order which allows to understand the logic
easily.
=== Add the new grammar to sdl92.g ===
The file <code>sdl92.g</code> contains the ANTLR3 grammar of the SDL language.
Please refer to the ANTLR documentation if needed.
We add the following production:
<pre>continuous_signal
: cif?
hyperlink?
PROVIDED expression e=end
(PRIORITY p=INT end)?
transition?
-&gt; ^(PROVIDED expression cif? hyperlink? $p? $e? transition?)
;</pre>
When a new token is needed (here: ''PROVIDED'') it must be added at several
places in the ANTLR grammar:
* in the &quot;symbolname&quot; production (for the CIF part)
* as a new keyword (<code>PROVIDED : P R O V I D E D</code>)
* in the list of tokens (tokens { ... })
'''Notes:'''
* ''cif'' and ''hyperlink'' are optional
* the ''transition'' is also optional, to allow partial model saving
* the expression is always the first child, since it does not have a dedicated
* token, and is the only mandatory field
* ''end'' corresponds to the ''COMMENT'' part in SDL
Then the new production is added as a child option to the (existing) ''state'':
<pre>state_part
: input_part
| save_part
| spontaneous_transition
| continuous_signal // &lt;==== HERE
| connect_part
;</pre>
=== Prepare the parser in ogParser.py ===
Find the parent rule (''state'' here) and add a branch to parse the new child.
Usually the rule is a function named after the production name. So look for
<code>def state (...)</code>
<pre>def state(root, parent, context):
'''
Parse a STATE.
&quot;parent&quot; is used to compute absolute coordinates
&quot;context&quot; is the AST used to store global data
(process/procedure)
'''</pre>
Each rule parses all its children based on the token name from ANTLR. It is
therefore straightforward to add the parsing of a new child:
<pre>for child in root.getChildren():
if child.type == lexer.CIF:
....
# you must add a new branch based on your token name:
elif child.type == lexer.PROVIDED:
# Add a placeholder for your code here
sterr.append('I am adding support for Continuous Signals now!')</pre>
If the new rule has children, you will likely want to add a new function to
parse it following the same scheme as the parent rule (add a function called
<code>continuous_signal</code> which returns the full content of the grammar).
We keep this as a placeholder for the time being because we must first define
if/what new entries are needed in the AST (<code>ogAST.py</code>).
'''Note:'''
In SDL there are family of features and very often, new features are similar to
existing ones so you will find a model to know how to easily code your new
function. In the case of the continuous signals, they are very similar to
<code>INPUT</code> and <code>CONNECT</code> - they are triggers for a transition
below a state.
=== Add new entries to the AST in ogAST.py ===
Find the relevant places and add new entries if needed. Here we look for the
State entry and we add a list of ''continuous signals''.
Depending on the complexity of the new feature, we can create a new class for
the entry, or use a simple type/dictionary.
The general rule is that if we are adding a new symbol (with coordinates,
comments, hyperlink, ...) it is better to create a new class to handle it. It is
the case here.
So first in the State class:
<pre>class State(object):
''' AST Entry for STATE symbols '''
def __init__(self, defName=''):
# (...) Add the following lines:
# list of ContinuousSignal (provided clauses below a state)
self.continuous_signals = []</pre>
Then create the new class, using the same structure as another class of a
feature of the same family - here, we inherit from Input. The class is very
simple:
<pre>class ContinuousSignal(Input):
''' AST Entry for the Continuous Signal '''
def __init__(self):
''' Difference with Input: trigger is an expression '''
super(ContinuousSignal, self).__init__()
# Expression triggering the transition
self.trigger = None
# Priority (integer)
self.priority = 0
def trace(self):
''' Debug output for a Continuous signal '''
return u'PROVIDED {exp} ({l},{c})'.format(exp=self.inputString,
l=self.line, c=self.charPositionInLine)</pre>
We are done with the AST, we can close <code>ogAST.py</code>.
=== Back to <code>ogParser.py</code> ===
Now we can parse the construct and create the AST entries we just defined. In
the state() function, we replace the placeholder we added at step 2:
<pre>(...)
elif child.type == lexer.PROVIDED:
# Continuous signal
provided_part, err, warn = continuous_signal(child, state_def, context)
state_def.continuous_signals.append(provided_part)
(...)</pre>
Then we can implement the continuous_signal function.
<pre>def continuous_signal(root, parent, context):
''' Parse a PROVIDED clause in a continuous signal '''
i = ogAST.ContinuousSignal()
(...)
return i, errors, warnings</pre>
At this point we are done with the parser, which is now capable of filling an
AST containing the new construct.
There are still a number of updates to perform to complete the support of the
new feature:
* Update the on-the-fly parser for syntax checks in the graphical editor
* (<code>ogParser.py</code>)
* Create a graphical symbol for the new feature (in <code>sdlSymbols.py</code>)
* Update the renderer to draw the symbol (<code>Renderer.py</code>)
* Update the backend that saves the model to phrase representation
* (<code>Pr.py</code>)
* Create an icon and add the new symbol to the palette
* Update the statechart renderer (<code>Statechart.py</code>)
* Update other backends that can be impacted, such as code generators
* (<code>AdaGenerator.py</code>, etc.)
* Update clipboard
=== Syntax checker in <code>ogParser.py</code> ===
When a graphical symbol is edited, a call to
<code>ogParser.parseSingleElement</code> is made to check that there are no
syntax errors.
There is an assertion at the begining of this function to prevent trying to
parse unsupported construct and raise a exception which would be more difficult
to debug.
We need to add our new &quot;continuous_signal&quot; symbol here:
<pre>assert(elem in ('input_part', 'output', 'decision', 'alternative_part',
'terminator_statement', 'label', 'task', 'procedure_call', 'end',
'text_area', 'state', 'start', 'procedure', 'floating_label',
'connect_part', 'process_definition', 'proc_start', 'state_start',
'signalroute', 'stop_if', 'continuous_signal'))</pre>
=== Create a new symbol in <code>sdlSymbols.py</code> ===
sdlSymbols.py module contains the SDL-specific symbol ; they all inherit from a
higher level symbol class, which provides the standard behaviour of the symbol,
e.g. when it is moved, etc.
The symbol-specific class must contain the following information:
* the name of the symbol as understood by the syntax checker
* the shape of the symbol
* the behaviour of the autocompletion feature
* the list of symbols that can be connected to this symbol
* and possibly tuning of some paramters
At the top-part of the module, the new class needs to be added to the __all__
global:
<pre>__all__ = ['Input', 'Output', 'State', 'Task', 'ProcedureCall', 'Label',
'Decision', 'DecisionAnswer', 'Join', 'Start', 'TextSymbol',
'Procedure', 'ProcedureStart', 'ProcedureStop', 'ASN1Viewer',
'StateStart', 'Process', 'ContinuousSignal']</pre>
Then the class is defined... extract:
<pre># pylint: disable=R0904
class ContinuousSignal(HorizontalSymbol):
''' &quot; Provided&quot; part below a state - not a enabling condition '''
common_name = 'continuous_signal' # &lt;==== Same name as in ogParser.py
def set_shape(self, width, height):
''' Define the shape '''
path = QPainterPath()
path.moveTo(15, 0)
path.lineTo(0, height / 2.0)
path.lineTo(15, height)
path.moveTo(width - 15, 0)
path.lineTo(width, height / 2.0)
path.lineTo(width - 15, height)
self.setPath(path)
super(ContinuousSignal, self).set_shape(width, height)</pre>
And you may need to add your new symbol to the properties of other symbols. For
example _insertable_followers gives the list of signals that the tool will allow
you to place after (or next to) the currently selected symbol.
The &quot;state&quot; symbol has this property:
<pre>_insertable_followers = ['Input', 'Connect', 'ContinuousSignal']</pre>
=== Update the renderer to see some first result: <code>Renderer.py</code> ===
Since the parser is ready and the symbol is defined, it is now possible to
update the graphical renderer and see if the symbol is properly displayed.
The renderer is a backend that uses a visitor design pattern (using the python
''singledispatch'' mechanism). It makes it easy to extend.
Rendering means taking an ogAST class and creating an sdlSymbols class. There is
a single <code>render</code> function that is dispatched depending on the class
name. You have to provide a function (name does not matter) and register it
using a singledispatch decorator:
<pre>@render.register(ogAST.ContinuousSignal)
def _continuous_signal(ast, scene, parent, states):
''' Add continuous signal to the scene '''
cont = sdlSymbols.ContinuousSignal(parent, ast=ast)
if cont not in scene.items():
add_to_scene(cont, scene)
if not parent:
cont.pos_x, cont.pos_y = ast.pos_x, ast.pos_y
if ast.transition:
render(ast.transition,
scene=scene,
parent=cont,
states=states)
return cont</pre>
And then you must call the render function from the State and Terminator
renderers, since the continuous signals are generated below states, just like
Inputs:
<pre>@render.register(ogAST.State)
def _state(ast, scene, states, terminators, parent=None):
(...)
for exit in chain(ast.inputs, ast.connects, ast.continuous_signals):
render(exit, scene=scene, parent=new_state, states=states)
(...)</pre>
=== Update the graphical parser backend (Pr.py) ===
In order to save the model or make syntax checks, we need to make sure that the
graphical model parser recognises the new symbol. The principle is the same as
for the renderer - a singledispatch visitor pattern, but on the graphical
symbols instead of the AST. It is therefore straightforward to parse - just add
a new function that mimicks an existing one and register it to singledispatch.
<pre>@generate.register(sdlSymbols.ContinuousSignal)
def _continuous_signal(symbol, recursive=True, **kwargs):
''' &quot;Provided&quot; symbol or branch if recursive is set '''
result = common('PROVIDED', symbol)
if recursive:
result.extend(recursive_aligned(symbol))
return result</pre>
As before, also make sure that the parent symbol (here: state) can recursively
parse the new child.
=== Create an icon for the palette ===
To be able to use the new symbol in the graphical editor you need to create an
icon that will be displayed in the palette of the GUI. The icons are specified
in SVG format (in the <code>icons/</code> directory) using the ''inkscape'' tool
and must be exporte to PNG.
The filename must be the same as the class name of your symbol, but with
lowercases.
Our class is named <code>ContinuousSignal</code> so we will create
<code>continuoussignal.png</code>.
The following properties apply when exporting to PNG:
* width 46
* height 35
* 90 DPI
Add the resulting filename inside the Qt resource file:
<code>opengeode.qrc</code>. This file lists all files that are
&quot;embedded&quot; in the application. This was done to avoid external files,
making the distribution of the tools easier to handle.
<pre>&lt;!DOCTYPE RCC&gt;&lt;RCC version=&quot;1.0&quot;&gt;
&lt;qresource&gt;
&lt;file&gt;icons/comment.png&lt;/file&gt;
&lt;file&gt;icons/decision.png&lt;/file&gt;
&lt;file&gt;icons/decisionanswer.png&lt;/file&gt;
&lt;file&gt;icons/input.png&lt;/file&gt;
... add the new file here (`continuoussignal.png`)</pre>
To compile this resource file to get a Python file, run:
<pre>make compile-all</pre>
You then need to specify in which palette(s) this new icon is available. This is
done in <code>opengeode.py</code>.
First add the new symbol (''ContinuousSignal'') to the list of imports:
<pre>from sdlSymbols import(Input, Output, Decision, DecisionAnswer, Task,
ProcedureCall, TextSymbol, State, Start, Join, Label, Procedure,
ProcedureStart, ProcedureStop, StateStart, Connect, Process,
ContinuousSignal)</pre>
Then edit the <code>ACTION</code> look-up table to specify which scenes can use
the symbol. In our case, the process, state and clipboard scenes. You can decide
at which place it will appear graphically - it respects the ordering of the
list.
=== Update the Statechart backend ===
Very few symbol changes imply a modification of the statechart renderer. It is
the case of the <code>continuous signals</code> because, like
<code>inputs</code>, they can trigger transitions.
The <code>Statechart.py</code> file is containing the code that creates a
graphviz translation of the SDL model to render a statechart.
The function that needs to be updated is <code>create_dot_graph</code>. It takes
a SDL model (instance of AST) and generates a number of graphviz models, one for
each level of hierarchy if the SDL model contains state composition or
aggregations.
What needs to be extended is the list of edges between the states.
It is easy here, we have to extend the creation of edges from:
<pre>for state, inputs in root_ast.mapping.viewitems():
# ... create edges triggered by inputs below states</pre>
to:
<pre>for state, inputs in chain(root_ast.mapping.viewitems(),
root_ast.cs_mapping.viewitems()):
# chain the input iterator with continuous signals</pre>
=== Update the code generator backends ===
All backends use a Visitor design pattern (with Python's ''singledispatch''
mechanism).
Updating backends with custom code generation features is therefore
straightforward. You need to find the parent function of your construct, and
create a new visitor function to generate the code corresponding you need (or
directly embed, depending on the complexity of the pattern).
The Ada code generator contains documentation and details on how to proceed with
updates. See <code>AdaGenerator.py</code> file.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment