namegen.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. from __future__ import annotations
  2. from typing import Iterable
  3. class NameGenerator:
  4. """Utility for generating distinct C names from Python names.
  5. Since C names can't use '.' (or unicode), some care is required to
  6. make C names generated from Python names unique. Also, we want to
  7. avoid generating overly long C names since they make the generated
  8. code harder to read.
  9. Note that we don't restrict ourselves to a 32-character distinguishing
  10. prefix guaranteed by the C standard since all the compilers we care
  11. about at the moment support longer names without issues.
  12. For names that are exported in a shared library (not static) use
  13. exported_name() instead.
  14. Summary of the approach:
  15. * Generate a unique name prefix from suffix of fully-qualified
  16. module name used for static names. If only compiling a single
  17. module, this can be empty. For example, if the modules are
  18. 'foo.bar' and 'foo.baz', the prefixes can be 'bar_' and 'baz_',
  19. respectively. If the modules are 'bar.foo' and 'baz.foo', the
  20. prefixes will be 'bar_foo_' and 'baz_foo_'.
  21. * Replace '.' in the Python name with '___' in the C name. (And
  22. replace the unlikely but possible '___' with '___3_'. This
  23. collides '___' with '.3_', but this is OK because names
  24. may not start with a digit.)
  25. The generated should be internal to a build and thus the mapping is
  26. arbitrary. Just generating names '1', '2', ... would be correct,
  27. though not very usable.
  28. """
  29. def __init__(self, groups: Iterable[list[str]]) -> None:
  30. """Initialize with a list of modules in each compilation group.
  31. The names of modules are used to shorten names referring to
  32. modules, for convenience. Arbitrary module
  33. names are supported for generated names, but uncompiled modules
  34. will use long names.
  35. """
  36. self.module_map: dict[str, str] = {}
  37. for names in groups:
  38. self.module_map.update(make_module_translation_map(names))
  39. self.translations: dict[tuple[str, str], str] = {}
  40. self.used_names: set[str] = set()
  41. def private_name(self, module: str, partial_name: str | None = None) -> str:
  42. """Return a C name usable for a static definition.
  43. Return a distinct result for each (module, partial_name) pair.
  44. The caller should add a suitable prefix to the name to avoid
  45. conflicts with other C names. Only ensure that the results of
  46. this function are unique, not that they aren't overlapping with
  47. arbitrary names.
  48. If a name is not specific to any module, the module argument can
  49. be an empty string.
  50. """
  51. # TODO: Support unicode
  52. if partial_name is None:
  53. return exported_name(self.module_map[module].rstrip("."))
  54. if (module, partial_name) in self.translations:
  55. return self.translations[module, partial_name]
  56. if module in self.module_map:
  57. module_prefix = self.module_map[module]
  58. elif module:
  59. module_prefix = module + "."
  60. else:
  61. module_prefix = ""
  62. actual = exported_name(f"{module_prefix}{partial_name}")
  63. self.translations[module, partial_name] = actual
  64. return actual
  65. def exported_name(fullname: str) -> str:
  66. """Return a C name usable for an exported definition.
  67. This is like private_name(), but the output only depends on the
  68. 'fullname' argument, so the names are distinct across multiple
  69. builds.
  70. """
  71. # TODO: Support unicode
  72. return fullname.replace("___", "___3_").replace(".", "___")
  73. def make_module_translation_map(names: list[str]) -> dict[str, str]:
  74. num_instances: dict[str, int] = {}
  75. for name in names:
  76. for suffix in candidate_suffixes(name):
  77. num_instances[suffix] = num_instances.get(suffix, 0) + 1
  78. result = {}
  79. for name in names:
  80. for suffix in candidate_suffixes(name):
  81. if num_instances[suffix] == 1:
  82. result[name] = suffix
  83. break
  84. else:
  85. assert False, names
  86. return result
  87. def candidate_suffixes(fullname: str) -> list[str]:
  88. components = fullname.split(".")
  89. result = [""]
  90. for i in range(len(components)):
  91. result.append(".".join(components[-i - 1 :]) + ".")
  92. return result