From e0e4c36715589a2398ce4e232de2395e81992587 Mon Sep 17 00:00:00 2001 From: Wolfgang Bangerth Date: Fri, 23 May 2025 09:31:24 -0600 Subject: [PATCH] Add two utility scripts that help debugging module problems. --- contrib/utilities/detect_module_cycles.py | 76 +++++++++++++++ .../find_module_partition_dependencies.py | 97 +++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 contrib/utilities/detect_module_cycles.py create mode 100644 contrib/utilities/find_module_partition_dependencies.py diff --git a/contrib/utilities/detect_module_cycles.py b/contrib/utilities/detect_module_cycles.py new file mode 100644 index 0000000000..620203d9db --- /dev/null +++ b/contrib/utilities/detect_module_cycles.py @@ -0,0 +1,76 @@ +#!/usr/bin/python3 + +## ------------------------------------------------------------------------ +## +## SPDX-License-Identifier: LGPL-2.1-or-later +## Copyright (C) 2025 by the deal.II authors +## +## This file is part of the deal.II library. +## +## Part of the source code is dual licensed under Apache-2.0 WITH +## LLVM-exception OR LGPL-2.1-or-later. Detailed license information +## governing the source code and code contributions can be found in +## LICENSE.md and CONTRIBUTING.md at the top level directory of deal.II. +## +## ------------------------------------------------------------------------ + +# Read through a list of the C++ files and see if we have any cycles +# in the 'export module' or 'module' and 'import' statements of these +# files. The script here assumes that all of the files export module +# partitions of the overall module 'dealii', and import module +# partitions from the same module. +# +# Call this script via +# python3 contrib/utilities/detect_module_cycles.py +# from the top-level directory of a build directory. + +from glob import glob +import networkx as nx +import re + + +match_imports = re.compile(r"import *: *(.*);") +match_exports = re.compile(r"export module dealii *: *(.*);") + +# For a given .cc file, read through all the lines and extract the +# ones that correspond to export or import statements. For those, add +# a link to the graph. +def add_imports_for_file(interface_module_partition_file, G) : + f = open(interface_module_partition_file) + lines = f.readlines() + f.close() + + for line in lines : + m = match_exports.match(line) + if m : + module_partition = m.group(1) + # There can only be one 'export' statement per file, so + # stop reading: + break + + for line in lines : + m = match_imports.match(line) + if m : + imported_partition = m.group(1) + G.add_edge(module_partition, + imported_partition) + + + +# Create a list of all source files in the build folder +filelist = glob("module/interface_module_partitions/**/*.cc*", recursive=True) +assert filelist, "Please call the script from the top-level directory." + +# For each header file, add the imports as the edges of a directed graph. +G = nx.DiGraph() +for interface_module_partition_file in filelist : + add_imports_for_file(interface_module_partition_file, G) + +# Then figure out whether there are cycles and if so, print them: +cycles = nx.simple_cycles(G) +cycles_as_list = list(cycles) +if (len(cycles_as_list) > 0) : + print (f"Cycles in the module partition graph detected!") + for cycle in cycles_as_list : + print(cycle) + exit(1) diff --git a/contrib/utilities/find_module_partition_dependencies.py b/contrib/utilities/find_module_partition_dependencies.py new file mode 100644 index 0000000000..6bdad6b65a --- /dev/null +++ b/contrib/utilities/find_module_partition_dependencies.py @@ -0,0 +1,97 @@ +#!/usr/bin/python3 + +## ------------------------------------------------------------------------ +## +## SPDX-License-Identifier: LGPL-2.1-or-later +## Copyright (C) 2025 by the deal.II authors +## +## This file is part of the deal.II library. +## +## Part of the source code is dual licensed under Apache-2.0 WITH +## LLVM-exception OR LGPL-2.1-or-later. Detailed license information +## governing the source code and code contributions can be found in +## LICENSE.md and CONTRIBUTING.md at the top level directory of deal.II. +## +## ------------------------------------------------------------------------ + +# Given a module partition name 'A' as the first argument, find all other module +# partitions 'B' it depends on either by way of direct +# import :B; +# statements, or because a module partition 'B' so imported itself has a statement +# of the form +# export import :C; +# +# Call this script via +# python3 contrib/utilities/find_module_partition_dependencies.py A +# where 'A' is the name of the module partition (which must be defined in +# one of the files in the current build directory). + + +from glob import glob +import networkx as nx +import re +import sys + +module_partition_name = sys.argv[1] +partition_to_filename = dict() + +match_imports = re.compile(r"(export )?import *: *(.*);") +match_exports = re.compile(r"(export )?module dealii *: *(.*);") + + +# For a given source file, read through all the lines and extract the +# ones that correspond to export or import statements. For those, add +# a link to the graph. +def add_imports_for_file(module_partition_file, G) : + f = open(module_partition_file) + lines = f.readlines() + f.close() + + module_partition = "" + for line in lines : + m = match_exports.match(line) + if m : + module_partition = m.group(2) + # There can only be one 'export' statement per file, so + # stop reading after recording the mapping from partition + # name to file name: + partition_to_filename[module_partition] = module_partition_file + break + assert module_partition != "", f"File <{module_partition_file} does not seem to export a module partition!" + + for line in lines : + m = match_imports.match(line) + if m : + imported_partition = m.group(2) + G.add_edge(module_partition, + imported_partition) + + + +# Create a list of all source files in the build folder +filelist = glob("module/*_module_partitions/**/*.cc*", recursive=True) +assert filelist, "Please call the script from the top-level build directory." + + +# For each header file, add the imports as the edges of a directed graph. +G = nx.DiGraph() +G.add_node(module_partition_name) # Make sure the graph has at least one node +for module_partition_file in filelist : + add_imports_for_file(module_partition_file, G) + +assert module_partition_name in partition_to_filename, \ + "The module partition given as argument could not be found in the module units found" + + +# Now find everything that is upstream of the module partition +# specified on the command line: +print(f"The dependencies of module partition '{module_partition_name}' are:") +for node in nx.dfs_postorder_nodes(G, source=module_partition_name) : + if node != module_partition_name : + if node in partition_to_filename : + print(f" {node}, implemented in {partition_to_filename[node]}") + else : + print(f" {node}, implemented in unknown file") + +n_dependencies = sum(1 for _ in nx.dfs_postorder_nodes(G, source=module_partition_name))-1 +print(f"{n_dependencies} partitions are upstream dependencies for '{module_partition_name}'") -- 2.39.5