doc: document CLI BNF grammar, add DFA figures

Technical details on CLI implementation.

Signed-off-by: Quentin Young <qlyoung@cumulusnetworks.com>
This commit is contained in:
Quentin Young 2018-02-13 18:09:58 -05:00
parent 79120ae8d6
commit e53d58537c
2 changed files with 308 additions and 58 deletions

View File

@ -66,10 +66,58 @@ parser is implemented in Bison and the lexer in Flex. These may be found in
to look. Bison is very stable and if it detects a syntax error, 99% of
the time it will be a syntax error in your definition.
The formal grammar in BNF is given below. This is the grammar implemented in
the Bison parser. At runtime, the Bison parser reads all of the CLI strings and
builds a combined directed graph that is used to match and interpret user
input.
Human-friendly explanations of how to use this grammar are given a bit later in
this section alongside information on the :ref:`cli-data-structures` constructed
by the parser.
.. productionlist::
command: `cmd_token_seq`
: `cmd_token_seq` `placeholder_token` "..."
cmd_token_seq: *empty*
: `cmd_token_seq` `cmd_token`
cmd_token: `simple_token`
: `selector`
simple_token: `literal_token`
: `placeholder_token`
literal_token: WORD `varname_token`
varname_token: "$" WORD
placeholder_token: `placeholder_token_real` `varname_token`
placeholder_token_real: IPV4
: IPV4_PREFIX
: IPV6
: IPV6_PREFIX
: VARIABLE
: RANGE
: MAC
: MAC_PREFIX
selector: "<" `selector_seq_seq` ">" `varname_token`
: "{" `selector_seq_seq` "}" `varname_token`
: "[" `selector_seq_seq` "]" `varname_token`
selector_seq_seq: `selector_seq_seq` "|" `selector_token_seq`
: `selector_token_seq`
selector_token_seq: `selector_token_seq` `selector_token`
: `selector_token`
selector_token: `selector`
: `simple_token`
Tokens
~~~~~~
The various capitalized tokens in the BNF above are in fact themselves
placeholders, but not defined as such in the formal grammar; the grammar
provides the structure, and the tokens are actually more like a type system for
the strings you write in your CLI definitions. A CLI definition string is
broken apart and each piece is assigned a type by the lexer based on a set of
regular expressions. The parser uses the type information to verify the string
and determine the structure of the CLI graph; additional metadata (such as the
raw text of each token) is encoded into the graph as it is constructed by the
parser, but this is merely a dumb copy job.
Each element in a command definition is assigned a type by the parser based on a set of regular expression rules.
Here is a brief summary of the various token types along with examples.
+-----------------+-----------------+-------------------------------------------------------------+
| Token type | Syntax | Description |
@ -382,6 +430,8 @@ In the examples below, each arrowed token needs a doc string.
"command <foo|bar> [example]"
^ ^ ^ ^
.. _cli-data-structures:
Data Structures
---------------
@ -401,76 +451,65 @@ self-contained 'subgraphs'. Each subgraph is a tree except that all of
the 'leaves' actually share a child node. This helps with minimizing
graph size and debugging.
As an example, the subgraph generated by looks like this:
As a working example, here is the graph of the following command: ::
::
show [ip] bgp neighbors [<A.B.C.D|X:X::X:X|WORD>] [json]
.
.
|
+----+---+
+--- -+ FORK +----+
| +--------+ |
+--v---+ +--v---+
| foo | | bar |
+--+---+ +--+---+
| +------+ |
+------> JOIN <-----+
+---+--+
|
.
.
.. figure:: ../figures/cligraph.svg
:align: center
FORK and JOIN nodes are plumbing nodes that don't correspond to user
Graph of example CLI command
``FORK`` and ``JOIN`` nodes are plumbing nodes that don't correspond to user
input. They're necessary in order to deduplicate these constructs where
applicable.
Options follow the same form, except that there is an edge from the FORK
node to the JOIN node.
Options follow the same form, except that there is an edge from the ``FORK``
node to the ``JOIN`` node. Since all of the subgraphs in the example command
are optional, all of them have this edge.
Keywords follow the same form, except that there is an edge from JOIN to
FORK. Because of this the CLI graph cannot be called acyclic. There is
special logic in the input matching code that keeps a stack of paths
already taken through the node in order to disallow following the same
path more than once.
Keywords follow the same form, except that there is an edge from ``JOIN`` to
``FORK``. Because of this the CLI graph cannot be called acyclic. There is
special logic in the input matching code that keeps a stack of paths already
taken through the node in order to disallow following the same path more than
once.
Variadics are a bit special; they have an edge back to themselves, which
allows repeating the same input indefinitely.
Variadics are a bit special; they have an edge back to themselves, which allows
repeating the same input indefinitely.
The leaves of the graph are nodes that have no out edges. These nodes
are special; their data section does not contain a token, as most nodes
do, or NULL, as in FORK/JOIN nodes, but instead has a pointer to a
The leaves of the graph are nodes that have no out edges. These nodes are
special; their data section does not contain a token, as most nodes do, or
NULL, as in ``FORK``/``JOIN`` nodes, but instead has a pointer to a
cmd\_element. All paths through the graph that terminate on a leaf are
guaranteed to be defined by that command. When a user enters a complete
command, the command matcher tokenizes the input and executes a DFS on
the CLI graph. If it is simultaneously able to exhaust all input (one
input token per graph node), and then find exactly one leaf connected to
the last node it reaches, then the input has matched the corresponding
command and the command is executed. If it finds more than one node,
then the command is ambiguous (more on this in deduplication). If it
cannot exhaust all input, the command is unknown. If it exhausts all
input but does not find an edge node, the command is incomplete.
command, the command matcher tokenizes the input and executes a DFS on the CLI
graph. If it is simultaneously able to exhaust all input (one input token per
graph node), and then find exactly one leaf connected to the last node it
reaches, then the input has matched the corresponding command and the command
is executed. If it finds more than one node, then the command is ambiguous
(more on this in deduplication). If it cannot exhaust all input, the command is
unknown. If it exhausts all input but does not find an edge node, the command
is incomplete.
The parser uses an incremental strategy to build the CLI graph for a
node. Each command is parsed into its own graph, and then this graph is
merged into the overall graph. During this merge step, the parser makes
a best-effort attempt to remove duplicate nodes. If it finds a node in
the overall graph that is equal to a node in the corresponding position
in the command graph, it will intelligently merge the properties from
the node in the command graph into the already-existing node. Subgraphs
are also checked for isomorphism and merged where possible. The
definition of whether two nodes are 'equal' is based on the equality of
some set of token properties; read the parser source for the most
The parser uses an incremental strategy to build the CLI graph for a node. Each
command is parsed into its own graph, and then this graph is merged into the
overall graph. During this merge step, the parser makes a best-effort attempt
to remove duplicate nodes. If it finds a node in the overall graph that is
equal to a node in the corresponding position in the command graph, it will
intelligently merge the properties from the node in the command graph into the
already-existing node. Subgraphs are also checked for isomorphism and merged
where possible. The definition of whether two nodes are 'equal' is based on the
equality of some set of token properties; read the parser source for the most
up-to-date definition of equality.
When the parser is unable to deduplicate some complicated constructs,
this can result in two identical paths through separate parts of the
graph. If this occurs and the user enters input that matches these
paths, they will receive an 'ambiguous command' error and will be unable
to execute the command. Most of the time the parser can detect and warn
about duplicate commands, but it will not always be able to do this.
Hence care should be taken before defining a new command to ensure it is
not defined elsewhere.
When the parser is unable to deduplicate some complicated constructs, this can
result in two identical paths through separate parts of the graph. If this
occurs and the user enters input that matches these paths, they will receive an
'ambiguous command' error and will be unable to execute the command. Most of
the time the parser can detect and warn about duplicate commands, but it will
not always be able to do this. Hence care should be taken before defining a
new command to ensure it is not defined elsewhere.
Command handlers
----------------
@ -481,7 +520,7 @@ this:
::
int (*func) (const struct cmd_element *, struct vty *, int, struct cmd_token *[]);
int (*func) (const struct cmd_element *, struct vty *, int, struct cmd_token *[]);
The first argument is the command definition struct. The last argument
is an ordered array of tokens that correspond to the path taken through

211
doc/figures/cligraph.svg Normal file
View File

@ -0,0 +1,211 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
-->
<!-- Title: %3 Pages: 1 -->
<svg width="300pt" height="980pt"
viewBox="0.00 0.00 299.50 980.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 976)">
<title>%3</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-976 295.5,-976 295.5,4 -4,4"/>
<!-- n0xd46960 -->
<g id="node1" class="node"><title>n0xd46960</title>
<polygon fill="#ccffcc" stroke="black" points="158,-972 86,-972 86,-936 158,-936 158,-972"/>
<text text-anchor="start" x="94" y="-952.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">START_TKN</text>
</g>
<!-- n0xd46be0 -->
<g id="node2" class="node"><title>n0xd46be0</title>
<polygon fill="#ffffff" stroke="black" points="159,-900 85,-900 85,-864 159,-864 159,-900"/>
<text text-anchor="start" x="93" y="-885.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">WORD_TKN</text>
<text text-anchor="start" x="101.5" y="-875.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
<text text-anchor="start" x="105.5" y="-875.2" font-family="Fira Mono" font-weight="bold" font-size="11.00" fill="#0055ff">show</text>
<text text-anchor="start" x="138.5" y="-875.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
</g>
<!-- n0xd46960&#45;&gt;n0xd46be0 -->
<g id="edge1" class="edge"><title>n0xd46960&#45;&gt;n0xd46be0</title>
<path fill="none" stroke="black" d="M122,-935.697C122,-927.983 122,-918.712 122,-910.112"/>
<polygon fill="black" stroke="black" points="125.5,-910.104 122,-900.104 118.5,-910.104 125.5,-910.104"/>
</g>
<!-- n0xd47f80 -->
<g id="node3" class="node"><title>n0xd47f80</title>
<polygon fill="#aaddff" stroke="black" points="156.5,-828 87.5,-828 87.5,-792 156.5,-792 156.5,-828"/>
<text text-anchor="start" x="95.5" y="-808.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">FORK_TKN</text>
</g>
<!-- n0xd46be0&#45;&gt;n0xd47f80 -->
<g id="edge2" class="edge"><title>n0xd46be0&#45;&gt;n0xd47f80</title>
<path fill="none" stroke="black" d="M122,-863.697C122,-855.983 122,-846.712 122,-838.112"/>
<polygon fill="black" stroke="black" points="125.5,-838.104 122,-828.104 118.5,-838.104 125.5,-838.104"/>
</g>
<!-- n0xd47c70 -->
<g id="node4" class="node"><title>n0xd47c70</title>
<polygon fill="#ffffff" stroke="black" points="127,-756 53,-756 53,-720 127,-720 127,-756"/>
<text text-anchor="start" x="61" y="-741.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">WORD_TKN</text>
<text text-anchor="start" x="80.5" y="-731.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
<text text-anchor="start" x="84.5" y="-731.2" font-family="Fira Mono" font-weight="bold" font-size="11.00" fill="#0055ff">ip</text>
<text text-anchor="start" x="95.5" y="-731.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
</g>
<!-- n0xd47f80&#45;&gt;n0xd47c70 -->
<g id="edge3" class="edge"><title>n0xd47f80&#45;&gt;n0xd47c70</title>
<path fill="none" stroke="black" d="M114.09,-791.697C110.447,-783.728 106.046,-774.1 102.006,-765.264"/>
<polygon fill="black" stroke="black" points="105.16,-763.744 97.8191,-756.104 98.7936,-766.654 105.16,-763.744"/>
</g>
<!-- n0xd484c0 -->
<g id="node5" class="node"><title>n0xd484c0</title>
<polygon fill="#ddaaff" stroke="black" points="153.5,-684 90.5,-684 90.5,-648 153.5,-648 153.5,-684"/>
<text text-anchor="start" x="98.5" y="-664.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">JOIN_TKN</text>
</g>
<!-- n0xd47f80&#45;&gt;n0xd484c0 -->
<g id="edge20" class="edge"><title>n0xd47f80&#45;&gt;n0xd484c0</title>
<path fill="none" stroke="black" d="M127.824,-791.56C130.931,-781.33 134.431,-768.08 136,-756 138.06,-740.133 138.06,-735.867 136,-720 134.897,-711.506 132.839,-702.434 130.634,-694.24"/>
<polygon fill="black" stroke="black" points="133.945,-693.087 127.824,-684.44 127.216,-695.017 133.945,-693.087"/>
</g>
<!-- n0xd47c70&#45;&gt;n0xd484c0 -->
<g id="edge4" class="edge"><title>n0xd47c70&#45;&gt;n0xd484c0</title>
<path fill="none" stroke="black" d="M97.9101,-719.697C101.553,-711.728 105.954,-702.1 109.994,-693.264"/>
<polygon fill="black" stroke="black" points="113.206,-694.654 114.181,-684.104 106.84,-691.744 113.206,-694.654"/>
</g>
<!-- n0xd47ca0 -->
<g id="node6" class="node"><title>n0xd47ca0</title>
<polygon fill="#ffffff" stroke="black" points="159,-612 85,-612 85,-576 159,-576 159,-612"/>
<text text-anchor="start" x="93" y="-597.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">WORD_TKN</text>
<text text-anchor="start" x="106.5" y="-587.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
<text text-anchor="start" x="110.5" y="-587.2" font-family="Fira Mono" font-weight="bold" font-size="11.00" fill="#0055ff">bgp</text>
<text text-anchor="start" x="133.5" y="-587.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
</g>
<!-- n0xd484c0&#45;&gt;n0xd47ca0 -->
<g id="edge5" class="edge"><title>n0xd484c0&#45;&gt;n0xd47ca0</title>
<path fill="none" stroke="black" d="M122,-647.697C122,-639.983 122,-630.712 122,-622.112"/>
<polygon fill="black" stroke="black" points="125.5,-622.104 122,-612.104 118.5,-622.104 125.5,-622.104"/>
</g>
<!-- n0xd48540 -->
<g id="node7" class="node"><title>n0xd48540</title>
<polygon fill="#ffffff" stroke="black" points="164.5,-540 79.5,-540 79.5,-504 164.5,-504 164.5,-540"/>
<text text-anchor="start" x="93" y="-525.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">WORD_TKN</text>
<text text-anchor="start" x="87.5" y="-515.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
<text text-anchor="start" x="91.5" y="-515.2" font-family="Fira Mono" font-weight="bold" font-size="11.00" fill="#0055ff">neighbors</text>
<text text-anchor="start" x="152.5" y="-515.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
</g>
<!-- n0xd47ca0&#45;&gt;n0xd48540 -->
<g id="edge6" class="edge"><title>n0xd47ca0&#45;&gt;n0xd48540</title>
<path fill="none" stroke="black" d="M122,-575.697C122,-567.983 122,-558.712 122,-550.112"/>
<polygon fill="black" stroke="black" points="125.5,-550.104 122,-540.104 118.5,-550.104 125.5,-550.104"/>
</g>
<!-- n0xd490c0 -->
<g id="node8" class="node"><title>n0xd490c0</title>
<polygon fill="#aaddff" stroke="black" points="156.5,-468 87.5,-468 87.5,-432 156.5,-432 156.5,-468"/>
<text text-anchor="start" x="95.5" y="-448.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">FORK_TKN</text>
</g>
<!-- n0xd48540&#45;&gt;n0xd490c0 -->
<g id="edge7" class="edge"><title>n0xd48540&#45;&gt;n0xd490c0</title>
<path fill="none" stroke="black" d="M122,-503.697C122,-495.983 122,-486.712 122,-478.112"/>
<polygon fill="black" stroke="black" points="125.5,-478.104 122,-468.104 118.5,-478.104 125.5,-478.104"/>
</g>
<!-- n0xd48fc0 -->
<g id="node9" class="node"><title>n0xd48fc0</title>
<polygon fill="#ffffff" stroke="black" points="64,-396 0,-396 0,-360 64,-360 64,-396"/>
<text text-anchor="start" x="8" y="-380.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">IPV4_TKN</text>
<text text-anchor="start" x="15" y="-371.8" font-family="Fira Mono" font-size="9.00">A.B.C.D</text>
</g>
<!-- n0xd490c0&#45;&gt;n0xd48fc0 -->
<g id="edge8" class="edge"><title>n0xd490c0&#45;&gt;n0xd48fc0</title>
<path fill="none" stroke="black" d="M99.7528,-431.697C88.4181,-422.881 74.4698,-412.032 62.1811,-402.474"/>
<polygon fill="black" stroke="black" points="64.0336,-399.481 53.9913,-396.104 59.736,-405.007 64.0336,-399.481"/>
</g>
<!-- n0xd491e0 -->
<g id="node10" class="node"><title>n0xd491e0</title>
<polygon fill="#ddaaff" stroke="black" points="153.5,-324 90.5,-324 90.5,-288 153.5,-288 153.5,-324"/>
<text text-anchor="start" x="98.5" y="-304.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">JOIN_TKN</text>
</g>
<!-- n0xd490c0&#45;&gt;n0xd491e0 -->
<g id="edge19" class="edge"><title>n0xd490c0&#45;&gt;n0xd491e0</title>
<path fill="none" stroke="black" d="M117.536,-431.953C115.065,-421.63 112.248,-408.153 111,-396 109.366,-380.084 109.366,-375.916 111,-360 111.877,-351.455 113.531,-342.255 115.294,-333.958"/>
<polygon fill="black" stroke="black" points="118.743,-334.573 117.536,-324.047 111.915,-333.028 118.743,-334.573"/>
</g>
<!-- n0xd49340 -->
<g id="node15" class="node"><title>n0xd49340</title>
<polygon fill="#ffffff" stroke="black" points="184,-396 120,-396 120,-360 184,-360 184,-396"/>
<text text-anchor="start" x="128" y="-380.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">IPV6_TKN</text>
<text text-anchor="start" x="135" y="-371.8" font-family="Fira Mono" font-size="9.00">X:X::X:X</text>
</g>
<!-- n0xd490c0&#45;&gt;n0xd49340 -->
<g id="edge15" class="edge"><title>n0xd490c0&#45;&gt;n0xd49340</title>
<path fill="none" stroke="black" d="M129.416,-431.697C132.794,-423.813 136.87,-414.304 140.623,-405.546"/>
<polygon fill="black" stroke="black" points="143.947,-406.675 144.67,-396.104 137.513,-403.917 143.947,-406.675"/>
</g>
<!-- n0xd49480 -->
<g id="node16" class="node"><title>n0xd49480</title>
<polygon fill="#ffffff" stroke="black" points="291.5,-396 202.5,-396 202.5,-360 291.5,-360 291.5,-396"/>
<text text-anchor="start" x="210.5" y="-380.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">VARIABLE_TKN</text>
<text text-anchor="start" x="233" y="-371.8" font-family="Fira Mono" font-size="9.00">WORD</text>
</g>
<!-- n0xd490c0&#45;&gt;n0xd49480 -->
<g id="edge17" class="edge"><title>n0xd490c0&#45;&gt;n0xd49480</title>
<path fill="none" stroke="black" d="M152.578,-431.876C169.074,-422.639 189.624,-411.131 207.336,-401.212"/>
<polygon fill="black" stroke="black" points="209.289,-404.13 216.304,-396.19 205.869,-398.022 209.289,-404.13"/>
</g>
<!-- n0xd48fc0&#45;&gt;n0xd491e0 -->
<g id="edge9" class="edge"><title>n0xd48fc0&#45;&gt;n0xd491e0</title>
<path fill="none" stroke="black" d="M54.2472,-359.697C65.5819,-350.881 79.5302,-340.032 91.8189,-330.474"/>
<polygon fill="black" stroke="black" points="94.264,-333.007 100.009,-324.104 89.9664,-327.481 94.264,-333.007"/>
</g>
<!-- n0xd496e0 -->
<g id="node11" class="node"><title>n0xd496e0</title>
<polygon fill="#aaddff" stroke="black" points="156.5,-252 87.5,-252 87.5,-216 156.5,-216 156.5,-252"/>
<text text-anchor="start" x="95.5" y="-232.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">FORK_TKN</text>
</g>
<!-- n0xd491e0&#45;&gt;n0xd496e0 -->
<g id="edge10" class="edge"><title>n0xd491e0&#45;&gt;n0xd496e0</title>
<path fill="none" stroke="black" d="M122,-287.697C122,-279.983 122,-270.712 122,-262.112"/>
<polygon fill="black" stroke="black" points="125.5,-262.104 122,-252.104 118.5,-262.104 125.5,-262.104"/>
</g>
<!-- n0xd495e0 -->
<g id="node12" class="node"><title>n0xd495e0</title>
<polygon fill="#ffffff" stroke="black" points="127,-180 53,-180 53,-144 127,-144 127,-180"/>
<text text-anchor="start" x="61" y="-165.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">WORD_TKN</text>
<text text-anchor="start" x="73.5" y="-155.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
<text text-anchor="start" x="77.5" y="-155.2" font-family="Fira Mono" font-weight="bold" font-size="11.00" fill="#0055ff">json</text>
<text text-anchor="start" x="102.5" y="-155.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
</g>
<!-- n0xd496e0&#45;&gt;n0xd495e0 -->
<g id="edge11" class="edge"><title>n0xd496e0&#45;&gt;n0xd495e0</title>
<path fill="none" stroke="black" d="M114.09,-215.697C110.447,-207.728 106.046,-198.1 102.006,-189.264"/>
<polygon fill="black" stroke="black" points="105.16,-187.744 97.8191,-180.104 98.7936,-190.654 105.16,-187.744"/>
</g>
<!-- n0xd497c0 -->
<g id="node13" class="node"><title>n0xd497c0</title>
<polygon fill="#ddaaff" stroke="black" points="153.5,-108 90.5,-108 90.5,-72 153.5,-72 153.5,-108"/>
<text text-anchor="start" x="98.5" y="-88.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">JOIN_TKN</text>
</g>
<!-- n0xd496e0&#45;&gt;n0xd497c0 -->
<g id="edge14" class="edge"><title>n0xd496e0&#45;&gt;n0xd497c0</title>
<path fill="none" stroke="black" d="M127.824,-215.56C130.931,-205.33 134.431,-192.08 136,-180 138.06,-164.133 138.06,-159.867 136,-144 134.897,-135.506 132.839,-126.434 130.634,-118.24"/>
<polygon fill="black" stroke="black" points="133.945,-117.087 127.824,-108.44 127.216,-119.017 133.945,-117.087"/>
</g>
<!-- n0xd495e0&#45;&gt;n0xd497c0 -->
<g id="edge12" class="edge"><title>n0xd495e0&#45;&gt;n0xd497c0</title>
<path fill="none" stroke="black" d="M97.9101,-143.697C101.553,-135.728 105.954,-126.1 109.994,-117.264"/>
<polygon fill="black" stroke="black" points="113.206,-118.654 114.181,-108.104 106.84,-115.744 113.206,-118.654"/>
</g>
<!-- end0xd49900 -->
<g id="node14" class="node"><title>end0xd49900</title>
<polygon fill="#ffddaa" stroke="black" points="149,-36 95,-36 95,-0 149,-0 149,-36"/>
<text text-anchor="start" x="112.5" y="-15.8" font-family="Fira Mono" font-size="9.00">end</text>
</g>
<!-- n0xd497c0&#45;&gt;end0xd49900 -->
<g id="edge13" class="edge"><title>n0xd497c0&#45;&gt;end0xd49900</title>
<path fill="none" stroke="black" d="M122,-71.6966C122,-63.9827 122,-54.7125 122,-46.1124"/>
<polygon fill="black" stroke="black" points="125.5,-46.1043 122,-36.1043 118.5,-46.1044 125.5,-46.1043"/>
</g>
<!-- n0xd49340&#45;&gt;n0xd491e0 -->
<g id="edge16" class="edge"><title>n0xd49340&#45;&gt;n0xd491e0</title>
<path fill="none" stroke="black" d="M144.584,-359.697C141.206,-351.813 137.13,-342.304 133.377,-333.546"/>
<polygon fill="black" stroke="black" points="136.487,-331.917 129.33,-324.104 130.053,-334.675 136.487,-331.917"/>
</g>
<!-- n0xd49480&#45;&gt;n0xd491e0 -->
<g id="edge18" class="edge"><title>n0xd49480&#45;&gt;n0xd491e0</title>
<path fill="none" stroke="black" d="M216.422,-359.876C199.926,-350.639 179.376,-339.131 161.664,-329.212"/>
<polygon fill="black" stroke="black" points="163.131,-326.022 152.696,-324.19 159.711,-332.13 163.131,-326.022"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB