@@ -40,6 +40,7 @@ def __init__(self, name):
4040 self .line_number = NO_LINE_NUMBER
4141 self .original_name = name
4242 self .is_exit_node = False
43+ self .is_last_node = False
4344 self .code = []
4445 self .loc = 0
4546
@@ -71,6 +72,7 @@ class CallGraph:
7172 def __init__ (self , log_file = sys .stderr ):
7273 self .nodes = {}
7374 self .log_file = log_file
75+ self .first_node = None
7476
7577 def GetOrCreateNode (self , name ):
7678 if name in self .nodes :
@@ -80,6 +82,45 @@ def GetOrCreateNode(self, name):
8082 self .nodes [name ] = node
8183 return node
8284
85+ def _MarkExitNodes (self ):
86+ # A node is an exit node if:
87+ # 1. it contains an "exit" command with no target
88+ # or
89+ # 2. it's reached from the starting node via "goto" or "nested" connections
90+ # and it contains an exit command or a "goto eof" command.
91+
92+ # Identify all nodes with an exit command with no targets.
93+ for node in self .nodes .values ():
94+ all_commands = set (itertools .chain .from_iterable (line .commands for line in node .code ))
95+ exit_cmd = Command ("exit" , "" )
96+ if exit_cmd in all_commands :
97+ node .is_exit_node = True
98+
99+ # Visit the call graph to find nodes satisfying condition #2.
100+ q = [self .first_node ]
101+ visited = set () # Used to avoid loops, since the call graph is not acyclic.
102+
103+ while q :
104+ cur = q .pop ()
105+ visited .add (cur .name )
106+
107+ # Evaluate condition for marking exit node.
108+ if cur .is_last_node :
109+ cur .is_exit_node = True
110+ else :
111+ all_commands = itertools .chain .from_iterable (line .commands for line in cur .code )
112+ for command in all_commands :
113+ if command [0 ] == "exit" or (command [0 ] == "goto" and command [1 ] == "eof" ):
114+ cur .is_exit_node = True
115+ break
116+
117+ for connection in cur .connections :
118+ if connection .dst not in self .nodes or connection .dst in visited :
119+ continue
120+ if connection .kind == "nested" or connection .kind == "goto" :
121+ q .append (self .nodes [connection .dst ])
122+
123+
83124 # Adds to each node information depending on the contents of the code, such as connections
84125 # deriving from goto/call commands and whether the node is terminating or not.
85126 def _AnnotateNode (self , node ):
@@ -136,38 +177,29 @@ def _AnnotateNode(self, node):
136177
137178 if command == "exit" and target == "" :
138179 line .terminating = True
139- node .is_exit_node = True
140180
141181 @staticmethod
142182 def Build (input_file , log_file = sys .stderr ):
143183 call_graph = CallGraph ._ParseSource (input_file , log_file )
144184 for node in call_graph .nodes .values ():
145185 call_graph ._AnnotateNode (node )
146186
147- # Find exit nodes.
148- last_node = max (call_graph .nodes .values (), key = lambda x : x .line_number )
149- print (u"{0} is the last node, marking it as exit node." .format (last_node .name ), file = log_file )
150- last_node .is_exit_node = True
151-
152- # If the last node's last statement is a goto not going towards eof, then
153- # it's not an exit node.
154- for line in reversed (last_node .code ):
155- if line .noop :
156- continue
157-
158- for command , target in line .commands :
159- if command == "goto" and target and target != "eof" :
160- last_node .is_exit_node = False
161- break
162-
163- # Prune away EOF if it is a virtual node (no line number) and there are no connections to it.
187+ # Prune away EOF if it is a virtual node (no line number) and there are no call/nested connections to it.
164188 eof = call_graph .GetOrCreateNode ("eof" )
165- if eof .line_number == NO_LINE_NUMBER :
166- all_connections = itertools .chain .from_iterable (n .connections for n in call_graph .nodes .values ())
167- destinations = set (c .dst for c in all_connections )
168- if "eof" not in destinations :
169- print (u"Removing the eof node, since there are no connections to it and it's not a real node" , file = log_file )
170- del call_graph .nodes ["eof" ]
189+ all_connections = itertools .chain .from_iterable (n .connections for n in call_graph .nodes .values ())
190+ destinations = set ((c .dst , c .kind ) for c in all_connections )
191+ if eof .line_number == NO_LINE_NUMBER and ("eof" , "call" ) not in destinations and ("eof" , "nested" ) not in destinations :
192+ print (u"Removing the eof node, since there are no call/nested connections to it and it's not a real node" , file = log_file )
193+ del call_graph .nodes ["eof" ]
194+ for node in call_graph .nodes .values ():
195+ eof_connections = [c for c in node .connections if c .dst == "eof" ]
196+ print (u"Removing {} eof connections in node {}" .format (len (eof_connections ), node .name ), file = log_file )
197+ for c in eof_connections :
198+ node .connections .remove (c )
199+
200+ # Warn the user if there are goto connections to eof, which will not be executed by CMD.
201+ if eof .line_number != NO_LINE_NUMBER and ("eof" , "goto" ) in destinations :
202+ print (u"WARNING: there are goto connections to eof, but CMD will not execute that code via goto." , file = log_file )
171203
172204 # Find and mark the "nested" connections.
173205 nodes = [n for n in call_graph .nodes .values () if n .line_number != NO_LINE_NUMBER ]
@@ -202,6 +234,12 @@ def Build(input_file, log_file=sys.stderr):
202234
203235 break
204236
237+ # Mark all exit nodes.
238+ last_node = max (call_graph .nodes .values (), key = lambda x : x .line_number )
239+ print (u"{0} is the last node, marking it as exit node." .format (last_node .name ), file = log_file )
240+ last_node .is_last_node = True
241+ call_graph ._MarkExitNodes ()
242+
205243 return call_graph
206244
207245 # Creates a call graph from an input file, parsing the file in blocks and creating
@@ -214,6 +252,7 @@ def _ParseSource(input_file, log_file=sys.stderr):
214252 # Special node to signal the start of the script.
215253 cur_node = call_graph .GetOrCreateNode ("__begin__" )
216254 cur_node .line_number = 1
255+ call_graph .first_node = cur_node
217256
218257 # Special node used by cmd to signal the end of the script.
219258 eof = call_graph .GetOrCreateNode ("eof" )
@@ -240,9 +279,10 @@ def _ParseSource(input_file, log_file=sys.stderr):
240279 # nodes with the same line number.
241280 if line_number == 1 :
242281 del call_graph .nodes ["__begin__" ]
282+ call_graph .first_node = next_node
243283
244284 cur_node = next_node
245285
246286 cur_node .AddCodeLine (line_number , line )
247287
248- return call_graph
288+ return call_graph
0 commit comments