blob: 963d8bf8f0fe8e211507da1e5522d95d86ac9bdc [file] [log] [blame]
Ben Olmsteadc0d77842019-07-31 17:34:05 -07001# Copyright 2019 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Checks for dependency cycles in Emboss IR."""
16
reventlov6731fc42019-10-03 15:23:13 -070017from compiler.util import error
Eric Rahm12c5e842024-03-22 09:37:24 -070018from compiler.util import ir_data
reventlov6731fc42019-10-03 15:23:13 -070019from compiler.util import ir_util
20from compiler.util import traverse_ir
Ben Olmsteadc0d77842019-07-31 17:34:05 -070021
22
reventlov4000cd02022-04-05 12:15:39 -070023def _add_reference_to_dependencies(reference, dependencies, name,
24 source_file_name, errors):
25 if reference.canonical_name.object_path[0] in {"$is_statically_sized",
26 "$static_size_in_bits",
27 "$next"}:
28 # This error is a bit opaque, but given that the compiler used to crash on
29 # this case -- for a couple of years -- and no one complained, it seems
30 # safe to assume that this is a rare error.
31 errors.append([
32 error.error(source_file_name, reference.source_location,
33 "Keyword `" + reference.canonical_name.object_path[0] +
34 "` may not be used in this context."),
35 ])
36 return
Ben Olmsteadc0d77842019-07-31 17:34:05 -070037 dependencies[name] |= {ir_util.hashable_form_of_reference(reference)}
38
39
40def _add_field_reference_to_dependencies(reference, dependencies, name):
41 dependencies[name] |= {ir_util.hashable_form_of_reference(reference.path[0])}
42
43
44def _add_name_to_dependencies(proto, dependencies):
45 name = ir_util.hashable_form_of_reference(proto.name)
46 dependencies.setdefault(name, set())
47 return {"name": name}
48
49
50def _find_dependencies(ir):
51 """Constructs a dependency graph for the entire IR."""
52 dependencies = {}
reventlov4000cd02022-04-05 12:15:39 -070053 errors = []
Ben Olmsteadc0d77842019-07-31 17:34:05 -070054 traverse_ir.fast_traverse_ir_top_down(
Eric Rahm12c5e842024-03-22 09:37:24 -070055 ir, [ir_data.Reference], _add_reference_to_dependencies,
Ben Olmsteadc0d77842019-07-31 17:34:05 -070056 # TODO(bolms): Add handling for references inside of attributes, once
57 # there are attributes with non-constant values.
58 skip_descendants_of={
Eric Rahm12c5e842024-03-22 09:37:24 -070059 ir_data.AtomicType, ir_data.Attribute, ir_data.FieldReference
Ben Olmsteadc0d77842019-07-31 17:34:05 -070060 },
61 incidental_actions={
Eric Rahm12c5e842024-03-22 09:37:24 -070062 ir_data.Field: _add_name_to_dependencies,
63 ir_data.EnumValue: _add_name_to_dependencies,
64 ir_data.RuntimeParameter: _add_name_to_dependencies,
Ben Olmsteadc0d77842019-07-31 17:34:05 -070065 },
reventlov4000cd02022-04-05 12:15:39 -070066 parameters={
67 "dependencies": dependencies,
68 "errors": errors,
69 })
Ben Olmsteadc0d77842019-07-31 17:34:05 -070070 traverse_ir.fast_traverse_ir_top_down(
Eric Rahm12c5e842024-03-22 09:37:24 -070071 ir, [ir_data.FieldReference], _add_field_reference_to_dependencies,
72 skip_descendants_of={ir_data.Attribute},
Ben Olmsteadc0d77842019-07-31 17:34:05 -070073 incidental_actions={
Eric Rahm12c5e842024-03-22 09:37:24 -070074 ir_data.Field: _add_name_to_dependencies,
75 ir_data.EnumValue: _add_name_to_dependencies,
76 ir_data.RuntimeParameter: _add_name_to_dependencies,
Ben Olmsteadc0d77842019-07-31 17:34:05 -070077 },
78 parameters={"dependencies": dependencies})
reventlov4000cd02022-04-05 12:15:39 -070079 return dependencies, errors
Ben Olmsteadc0d77842019-07-31 17:34:05 -070080
81
82def _find_dependency_ordering_for_fields_in_structure(
83 structure, type_definition, dependencies):
84 """Populates structure.fields_in_dependency_order."""
85 # For fields which appear before their dependencies in the original source
86 # text, this algorithm moves them to immediately after their dependencies.
87 #
88 # This is one of many possible schemes for constructing a dependency ordering;
89 # it has the advantage that all of the generated fields (e.g., $size_in_bytes)
90 # stay at the end of the ordering, which makes testing easier.
91 order = []
92 added = set()
93 for parameter in type_definition.runtime_parameter:
94 added.add(ir_util.hashable_form_of_reference(parameter.name))
95 needed = list(range(len(structure.field)))
96 while True:
97 for i in range(len(needed)):
98 field_number = needed[i]
99 field = ir_util.hashable_form_of_reference(
100 structure.field[field_number].name)
101 assert field in dependencies, "dependencies = {}".format(dependencies)
102 if all(dependency in added for dependency in dependencies[field]):
103 order.append(field_number)
104 added.add(field)
105 del needed[i]
106 break
107 else:
108 break
109 # If a non-local-field dependency were in dependencies[field], then not all
110 # fields would be added to the dependency ordering. This shouldn't happen.
111 assert len(order) == len(structure.field), (
112 "order: {}\nlen(structure.field: {})".format(order, len(structure.field)))
113 del structure.fields_in_dependency_order[:]
114 structure.fields_in_dependency_order.extend(order)
115
116
117def _find_dependency_ordering_for_fields(ir):
118 """Populates the fields_in_dependency_order fields throughout ir."""
119 dependencies = {}
120 # TODO(bolms): This duplicates work in _find_dependencies that could be
121 # shared.
122 traverse_ir.fast_traverse_ir_top_down(
Eric Rahm12c5e842024-03-22 09:37:24 -0700123 ir, [ir_data.FieldReference], _add_field_reference_to_dependencies,
124 skip_descendants_of={ir_data.Attribute},
Ben Olmsteadc0d77842019-07-31 17:34:05 -0700125 incidental_actions={
Eric Rahm12c5e842024-03-22 09:37:24 -0700126 ir_data.Field: _add_name_to_dependencies,
127 ir_data.EnumValue: _add_name_to_dependencies,
128 ir_data.RuntimeParameter: _add_name_to_dependencies,
Ben Olmsteadc0d77842019-07-31 17:34:05 -0700129 },
130 parameters={"dependencies": dependencies})
131 traverse_ir.fast_traverse_ir_top_down(
Eric Rahm12c5e842024-03-22 09:37:24 -0700132 ir, [ir_data.Structure],
Ben Olmsteadc0d77842019-07-31 17:34:05 -0700133 _find_dependency_ordering_for_fields_in_structure,
134 parameters={"dependencies": dependencies})
135
136
137def _find_module_import_dependencies(ir):
138 """Constructs a dependency graph of module imports."""
139 dependencies = {}
140 for module in ir.module:
141 foreign_imports = set()
142 for foreign_import in module.foreign_import:
143 # The prelude gets an automatic self-import that shouldn't cause any
144 # problems. No other self-imports are allowed, however.
145 if foreign_import.file_name.text or module.source_file_name:
146 foreign_imports |= {(foreign_import.file_name.text,)}
147 dependencies[module.source_file_name,] = foreign_imports
148 return dependencies
149
150
151def _find_cycles(graph):
152 """Finds cycles in graph.
153
154 The graph does not need to be fully connected.
155
156 Arguments:
157 graph: A dictionary whose keys are node labels. Values are sets of node
158 labels, representing edges from the key node to the value nodes.
159
160 Returns:
161 A set of sets of nodes which form strongly-connected components (subgraphs
162 where every node is directly or indirectly reachable from every other node).
163 No node will be included in more than one strongly-connected component, by
164 definition. Strongly-connected components of size 1, where the node in the
165 component does not have a self-edge, are not included in the result.
166
167 Note that a strongly-connected component may have a more complex structure
168 than a single loop. For example:
169
170 +-- A <-+ +-> B --+
171 | | | |
172 v C v
173 D ^ ^ E
174 | | | |
175 +-> F --+ +-- G <-+
176 """
177 # This uses Tarjan's strongly-connected components algorithm, as described by
178 # Wikipedia. This is a depth-first traversal of the graph with a node stack
179 # that is independent of the call stack; nodes are added to the stack when
180 # they are first encountered, but not removed until all nodes they can reach
181 # have been checked.
182 next_index = [0]
183 node_indices = {}
184 node_lowlinks = {}
185 nodes_on_stack = set()
186 stack = []
187 nontrivial_components = set()
188
189 def strong_connect(node):
190 """Implements the STRONGCONNECT routine of Tarjan's algorithm."""
191 node_indices[node] = next_index[0]
192 node_lowlinks[node] = next_index[0]
193 next_index[0] += 1
194 stack.append(node)
195 nodes_on_stack.add(node)
196
197 for destination_node in graph[node]:
198 if destination_node not in node_indices:
199 strong_connect(destination_node)
200 node_lowlinks[node] = min(node_lowlinks[node],
201 node_lowlinks[destination_node])
202 elif destination_node in nodes_on_stack:
203 node_lowlinks[node] = min(node_lowlinks[node],
204 node_indices[destination_node])
205
206 strongly_connected_component = []
207 if node_lowlinks[node] == node_indices[node]:
208 while True:
209 popped_node = stack.pop()
210 nodes_on_stack.remove(popped_node)
211 strongly_connected_component.append(popped_node)
212 if popped_node == node:
213 break
214 if (len(strongly_connected_component) > 1 or
215 strongly_connected_component[0] in
216 graph[strongly_connected_component[0]]):
217 nontrivial_components.add(frozenset(strongly_connected_component))
218
219 for node in graph:
220 if node not in node_indices:
221 strong_connect(node)
222 return nontrivial_components
223
224
225def _find_object_dependency_cycles(ir):
226 """Finds dependency cycles in types in the ir."""
reventlov39468d02022-04-05 13:53:56 -0700227 dependencies, find_dependency_errors = _find_dependencies(ir)
228 if find_dependency_errors:
229 return find_dependency_errors
230 errors = []
Ben Olmsteadc0d77842019-07-31 17:34:05 -0700231 cycles = _find_cycles(dict(dependencies))
Ben Olmsteadc0d77842019-07-31 17:34:05 -0700232 for cycle in cycles:
233 # TODO(bolms): This lists the entire strongly-connected component in a
234 # fairly arbitrary order. This is simple, and handles components that
235 # aren't simple cycles, but may not be the most user-friendly way to
236 # present this information.
237 cycle_list = sorted(list(cycle))
238 node_object = ir_util.find_object(cycle_list[0], ir)
239 error_group = [
240 error.error(cycle_list[0][0], node_object.source_location,
241 "Dependency cycle\n" + node_object.name.name.text)
242 ]
243 for node in cycle_list[1:]:
244 node_object = ir_util.find_object(node, ir)
245 error_group.append(error.note(node[0], node_object.source_location,
246 node_object.name.name.text))
247 errors.append(error_group)
248 return errors
249
250
251def _find_module_dependency_cycles(ir):
252 """Finds dependency cycles in modules in the ir."""
253 dependencies = _find_module_import_dependencies(ir)
254 cycles = _find_cycles(dict(dependencies))
255 errors = []
256 for cycle in cycles:
257 cycle_list = sorted(list(cycle))
258 module = ir_util.find_object(cycle_list[0], ir)
259 error_group = [
260 error.error(cycle_list[0][0], module.source_location,
261 "Import dependency cycle\n" + module.source_file_name)
262 ]
263 for module_name in cycle_list[1:]:
264 module = ir_util.find_object(module_name, ir)
265 error_group.append(error.note(module_name[0], module.source_location,
266 module.source_file_name))
267 errors.append(error_group)
268 return errors
269
270
271def find_dependency_cycles(ir):
272 """Finds any dependency cycles in the ir."""
273 errors = _find_module_dependency_cycles(ir)
274 return errors + _find_object_dependency_cycles(ir)
275
276
277def set_dependency_order(ir):
278 """Sets the fields_in_dependency_order member of Structures."""
279 _find_dependency_ordering_for_fields(ir)
280 return []