Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 1 | # 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 | |
reventlov | 6731fc4 | 2019-10-03 15:23:13 -0700 | [diff] [blame] | 17 | from compiler.util import error |
Eric Rahm | 12c5e84 | 2024-03-22 09:37:24 -0700 | [diff] [blame] | 18 | from compiler.util import ir_data |
reventlov | 6731fc4 | 2019-10-03 15:23:13 -0700 | [diff] [blame] | 19 | from compiler.util import ir_util |
| 20 | from compiler.util import traverse_ir |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 21 | |
| 22 | |
reventlov | 4000cd0 | 2022-04-05 12:15:39 -0700 | [diff] [blame] | 23 | def _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 Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 37 | dependencies[name] |= {ir_util.hashable_form_of_reference(reference)} |
| 38 | |
| 39 | |
| 40 | def _add_field_reference_to_dependencies(reference, dependencies, name): |
| 41 | dependencies[name] |= {ir_util.hashable_form_of_reference(reference.path[0])} |
| 42 | |
| 43 | |
| 44 | def _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 | |
| 50 | def _find_dependencies(ir): |
| 51 | """Constructs a dependency graph for the entire IR.""" |
| 52 | dependencies = {} |
reventlov | 4000cd0 | 2022-04-05 12:15:39 -0700 | [diff] [blame] | 53 | errors = [] |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 54 | traverse_ir.fast_traverse_ir_top_down( |
Eric Rahm | 12c5e84 | 2024-03-22 09:37:24 -0700 | [diff] [blame] | 55 | ir, [ir_data.Reference], _add_reference_to_dependencies, |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 56 | # TODO(bolms): Add handling for references inside of attributes, once |
| 57 | # there are attributes with non-constant values. |
| 58 | skip_descendants_of={ |
Eric Rahm | 12c5e84 | 2024-03-22 09:37:24 -0700 | [diff] [blame] | 59 | ir_data.AtomicType, ir_data.Attribute, ir_data.FieldReference |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 60 | }, |
| 61 | incidental_actions={ |
Eric Rahm | 12c5e84 | 2024-03-22 09:37:24 -0700 | [diff] [blame] | 62 | ir_data.Field: _add_name_to_dependencies, |
| 63 | ir_data.EnumValue: _add_name_to_dependencies, |
| 64 | ir_data.RuntimeParameter: _add_name_to_dependencies, |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 65 | }, |
reventlov | 4000cd0 | 2022-04-05 12:15:39 -0700 | [diff] [blame] | 66 | parameters={ |
| 67 | "dependencies": dependencies, |
| 68 | "errors": errors, |
| 69 | }) |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 70 | traverse_ir.fast_traverse_ir_top_down( |
Eric Rahm | 12c5e84 | 2024-03-22 09:37:24 -0700 | [diff] [blame] | 71 | ir, [ir_data.FieldReference], _add_field_reference_to_dependencies, |
| 72 | skip_descendants_of={ir_data.Attribute}, |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 73 | incidental_actions={ |
Eric Rahm | 12c5e84 | 2024-03-22 09:37:24 -0700 | [diff] [blame] | 74 | ir_data.Field: _add_name_to_dependencies, |
| 75 | ir_data.EnumValue: _add_name_to_dependencies, |
| 76 | ir_data.RuntimeParameter: _add_name_to_dependencies, |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 77 | }, |
| 78 | parameters={"dependencies": dependencies}) |
reventlov | 4000cd0 | 2022-04-05 12:15:39 -0700 | [diff] [blame] | 79 | return dependencies, errors |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 80 | |
| 81 | |
| 82 | def _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 | |
| 117 | def _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 Rahm | 12c5e84 | 2024-03-22 09:37:24 -0700 | [diff] [blame] | 123 | ir, [ir_data.FieldReference], _add_field_reference_to_dependencies, |
| 124 | skip_descendants_of={ir_data.Attribute}, |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 125 | incidental_actions={ |
Eric Rahm | 12c5e84 | 2024-03-22 09:37:24 -0700 | [diff] [blame] | 126 | ir_data.Field: _add_name_to_dependencies, |
| 127 | ir_data.EnumValue: _add_name_to_dependencies, |
| 128 | ir_data.RuntimeParameter: _add_name_to_dependencies, |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 129 | }, |
| 130 | parameters={"dependencies": dependencies}) |
| 131 | traverse_ir.fast_traverse_ir_top_down( |
Eric Rahm | 12c5e84 | 2024-03-22 09:37:24 -0700 | [diff] [blame] | 132 | ir, [ir_data.Structure], |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 133 | _find_dependency_ordering_for_fields_in_structure, |
| 134 | parameters={"dependencies": dependencies}) |
| 135 | |
| 136 | |
| 137 | def _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 | |
| 151 | def _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 | |
| 225 | def _find_object_dependency_cycles(ir): |
| 226 | """Finds dependency cycles in types in the ir.""" |
reventlov | 39468d0 | 2022-04-05 13:53:56 -0700 | [diff] [blame] | 227 | dependencies, find_dependency_errors = _find_dependencies(ir) |
| 228 | if find_dependency_errors: |
| 229 | return find_dependency_errors |
| 230 | errors = [] |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 231 | cycles = _find_cycles(dict(dependencies)) |
Ben Olmstead | c0d7784 | 2019-07-31 17:34:05 -0700 | [diff] [blame] | 232 | 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 | |
| 251 | def _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 | |
| 271 | def 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 | |
| 277 | def set_dependency_order(ir): |
| 278 | """Sets the fields_in_dependency_order member of Structures.""" |
| 279 | _find_dependency_ordering_for_fields(ir) |
| 280 | return [] |