genesispy 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. genesispy-0.6.0/LICENSE +24 -0
  2. genesispy-0.6.0/PKG-INFO +114 -0
  3. genesispy-0.6.0/README.md +84 -0
  4. genesispy-0.6.0/pyproject.toml +69 -0
  5. genesispy-0.6.0/setup.cfg +4 -0
  6. genesispy-0.6.0/src/genesispy/__init__.py +3 -0
  7. genesispy-0.6.0/src/genesispy/_scalars.py +50 -0
  8. genesispy-0.6.0/src/genesispy/cache.py +208 -0
  9. genesispy-0.6.0/src/genesispy/cli.py +851 -0
  10. genesispy-0.6.0/src/genesispy/config_handler.py +889 -0
  11. genesispy-0.6.0/src/genesispy/extensions.py +91 -0
  12. genesispy-0.6.0/src/genesispy/gvpy_cli.py +505 -0
  13. genesispy-0.6.0/src/genesispy/hashing.py +60 -0
  14. genesispy-0.6.0/src/genesispy/json_io.py +74 -0
  15. genesispy-0.6.0/src/genesispy/manager.py +572 -0
  16. genesispy-0.6.0/src/genesispy/output_writer.py +535 -0
  17. genesispy-0.6.0/src/genesispy/reporting.py +183 -0
  18. genesispy-0.6.0/src/genesispy/template/__init__.py +0 -0
  19. genesispy-0.6.0/src/genesispy/template/aliases.py +166 -0
  20. genesispy-0.6.0/src/genesispy/template/emitter.py +138 -0
  21. genesispy-0.6.0/src/genesispy/template/parser.py +744 -0
  22. genesispy-0.6.0/src/genesispy/template/runtime.py +113 -0
  23. genesispy-0.6.0/src/genesispy/tools/__init__.py +0 -0
  24. genesispy-0.6.0/src/genesispy/tools/jinja2j2.py +681 -0
  25. genesispy-0.6.0/src/genesispy/tools/json2xml.py +6 -0
  26. genesispy-0.6.0/src/genesispy/tools/vp2vpy.py +2046 -0
  27. genesispy-0.6.0/src/genesispy/tools/vp2vpy_helper.pl +109 -0
  28. genesispy-0.6.0/src/genesispy/tools/vp2vpy_map.py +253 -0
  29. genesispy-0.6.0/src/genesispy/tools/xml2json.py +6 -0
  30. genesispy-0.6.0/src/genesispy/tools/xml_json.py +398 -0
  31. genesispy-0.6.0/src/genesispy/unique_module.py +1463 -0
  32. genesispy-0.6.0/src/genesispy/user_config.py +189 -0
  33. genesispy-0.6.0/src/genesispy/user_lib.py +21 -0
  34. genesispy-0.6.0/src/genesispy.egg-info/PKG-INFO +114 -0
  35. genesispy-0.6.0/src/genesispy.egg-info/SOURCES.txt +82 -0
  36. genesispy-0.6.0/src/genesispy.egg-info/dependency_links.txt +1 -0
  37. genesispy-0.6.0/src/genesispy.egg-info/entry_points.txt +7 -0
  38. genesispy-0.6.0/src/genesispy.egg-info/requires.txt +15 -0
  39. genesispy-0.6.0/src/genesispy.egg-info/top_level.txt +1 -0
  40. genesispy-0.6.0/tests/test_alias_prelude.py +156 -0
  41. genesispy-0.6.0/tests/test_alias_runtime.py +134 -0
  42. genesispy-0.6.0/tests/test_cache.py +37 -0
  43. genesispy-0.6.0/tests/test_cfg_include_dispatch.py +150 -0
  44. genesispy-0.6.0/tests/test_cfg_sandbox_additions.py +69 -0
  45. genesispy-0.6.0/tests/test_cli.py +721 -0
  46. genesispy-0.6.0/tests/test_cli_log_product.py +101 -0
  47. genesispy-0.6.0/tests/test_clone_synonym_gaps.py +158 -0
  48. genesispy-0.6.0/tests/test_config_handler.py +357 -0
  49. genesispy-0.6.0/tests/test_demos_e2e.py +295 -0
  50. genesispy-0.6.0/tests/test_demos_make.py +299 -0
  51. genesispy-0.6.0/tests/test_demos_smoke.py +86 -0
  52. genesispy-0.6.0/tests/test_emitter.py +133 -0
  53. genesispy-0.6.0/tests/test_error_warning.py +131 -0
  54. genesispy-0.6.0/tests/test_errors.py +156 -0
  55. genesispy-0.6.0/tests/test_force_param_pin.py +69 -0
  56. genesispy-0.6.0/tests/test_generate_aliases.py +113 -0
  57. genesispy-0.6.0/tests/test_gvpy_cli.py +529 -0
  58. genesispy-0.6.0/tests/test_gvpy_demo.py +128 -0
  59. genesispy-0.6.0/tests/test_hashing.py +67 -0
  60. genesispy-0.6.0/tests/test_hierarchy_snapshot.py +196 -0
  61. genesispy-0.6.0/tests/test_j2_e2e.py +107 -0
  62. genesispy-0.6.0/tests/test_jinja2j2.py +390 -0
  63. genesispy-0.6.0/tests/test_json_io.py +95 -0
  64. genesispy-0.6.0/tests/test_manager_integration.py +299 -0
  65. genesispy-0.6.0/tests/test_manager_skeleton.py +137 -0
  66. genesispy-0.6.0/tests/test_output_suffix.py +243 -0
  67. genesispy-0.6.0/tests/test_output_writer.py +637 -0
  68. genesispy-0.6.0/tests/test_parameter_accessors.py +68 -0
  69. genesispy-0.6.0/tests/test_parameter_kwargs.py +242 -0
  70. genesispy-0.6.0/tests/test_parity_normalize.py +304 -0
  71. genesispy-0.6.0/tests/test_parity_smoke.py +179 -0
  72. genesispy-0.6.0/tests/test_parser.py +288 -0
  73. genesispy-0.6.0/tests/test_parser_j2.py +453 -0
  74. genesispy-0.6.0/tests/test_scalars.py +62 -0
  75. genesispy-0.6.0/tests/test_strcallable_shortnames.py +70 -0
  76. genesispy-0.6.0/tests/test_subinst_api.py +212 -0
  77. genesispy-0.6.0/tests/test_synonym_overload.py +218 -0
  78. genesispy-0.6.0/tests/test_to_string.py +45 -0
  79. genesispy-0.6.0/tests/test_tools_xml_json.py +143 -0
  80. genesispy-0.6.0/tests/test_unique_module.py +910 -0
  81. genesispy-0.6.0/tests/test_unwrap.py +167 -0
  82. genesispy-0.6.0/tests/test_user_config.py +341 -0
  83. genesispy-0.6.0/tests/test_vp2vpy_smoke.py +99 -0
  84. genesispy-0.6.0/tests/test_vp2vpy_units.py +704 -0
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <https://unlicense.org/>
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: genesispy
3
+ Version: 0.6.0
4
+ Summary: Genesis Chip Generator, Python port of Genesis2
5
+ Author: stfns-s
6
+ License-Expression: Unlicense
7
+ Project-URL: Homepage, https://github.com/stfns-s/genesispy
8
+ Project-URL: Repository, https://github.com/stfns-s/genesispy
9
+ Keywords: RTL generator,verilog,ASIC,FPGA
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: xml
19
+ Requires-Dist: lxml>=4.9; extra == "xml"
20
+ Provides-Extra: color
21
+ Requires-Dist: colorama>=0.4; extra == "color"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7; extra == "dev"
24
+ Requires-Dist: fixedpoint>=1.0; extra == "dev"
25
+ Requires-Dist: lxml>=4.9; extra == "dev"
26
+ Requires-Dist: colorama>=0.4; extra == "dev"
27
+ Provides-Extra: import-j2
28
+ Requires-Dist: jinja2>=3.1; extra == "import-j2"
29
+ Dynamic: license-file
30
+
31
+ # genesispy
32
+
33
+ Python port of the Genesis2 Chip Generator (see
34
+ <https://github.com/StanfordVLSI/Genesis2>). Replaces the Perl runtime
35
+ with a pure-Python implementation while keeping the template hierarchy
36
+ (now `.vpy` / `.svpy`). Configuration is JSON; legacy Genesis2 `.xml`
37
+ configs convert via the bundled `genesispy-xml2json` helper.
38
+
39
+ ## Install
40
+
41
+ ### From PyPI
42
+
43
+ ```sh
44
+ pip install genesispy
45
+ ```
46
+
47
+ Installs the `genesispy`, `gvpy`, `genesispy-vp2vpy`, `genesispy-xml2json`,
48
+ `genesispy-json2xml` and `genesispy-jinja2j2` commands. There are no
49
+ mandatory dependencies; three optional extras:
50
+
51
+ ```sh
52
+ pip install 'genesispy[xml]' # lxml: pretty-printed genesispy-json2xml
53
+ pip install 'genesispy[color]' # colorama: coloured diagnostics
54
+ pip install 'genesispy[import-j2]' # jinja2: required by genesispy-jinja2j2
55
+ ```
56
+
57
+ ### From a checkout, without pip (recommended for developers -- `bin/` launchers)
58
+
59
+ The repo ships shell launchers in `bin/` (`bin/genesispy`, `bin/gvpy`)
60
+ that set `PYTHONPATH` to the sibling `src/` and exec
61
+ `python3 -m genesispy.cli`. No build step, no pip -- just run from a
62
+ checkout:
63
+
64
+ ```sh
65
+ ./bin/genesispy --input top.vpy --top top --json-cfg config.json
66
+ ```
67
+
68
+ To "install" to a destination, copy `bin/` and `src/` together (the
69
+ launchers resolve `src/` as `../src` relative to their own directory):
70
+
71
+ ```sh
72
+ DEST=/path/to/install-dir
73
+ mkdir -p "$DEST"
74
+ cp -a bin src "$DEST/"
75
+ export PATH="$DEST/bin:$PATH"
76
+ ```
77
+
78
+ ### From a checkout, with pip
79
+
80
+ ```sh
81
+ pip install -e .
82
+ # or
83
+ pip install --target /path/to/install-dir .
84
+ ```
85
+
86
+ ## Tests
87
+
88
+ Against the in-tree source, no install required:
89
+
90
+ ```sh
91
+ PYTHONPATH=src pytest tests/
92
+ ```
93
+
94
+ Against an installed copy (`pip install -e .` or `pip install genesispy`):
95
+
96
+ ```sh
97
+ pytest tests/
98
+ ```
99
+
100
+ ## Documentation
101
+
102
+ Each entry links to the rendered copy on GitHub; the `local` link resolves in a
103
+ source checkout.
104
+
105
+ - [doc/user-guide.md](https://github.com/stfns-s/genesispy/blob/main/doc/user-guide.md)
106
+ ([local](./doc/user-guide.md)) -- `.vpy` syntax, walkthrough, CLI reference
107
+ for `genesispy` and `gvpy`, migrating from Genesis2.
108
+ - [doc/code-structure.md](https://github.com/stfns-s/genesispy/blob/main/doc/code-structure.md)
109
+ ([local](./doc/code-structure.md)) -- pipeline, dedup, control flags.
110
+ - [doc/interfaces.md](https://github.com/stfns-s/genesispy/blob/main/doc/interfaces.md)
111
+ ([local](./doc/interfaces.md)) -- module-boundary interfaces.
112
+ - [doc/genesis2-incompatibilities.md](https://github.com/stfns-s/genesispy/blob/main/doc/genesis2-incompatibilities.md)
113
+ ([local](./doc/genesis2-incompatibilities.md)) -- non-obvious behavior
114
+ differences from Perl Genesis2.
@@ -0,0 +1,84 @@
1
+ # genesispy
2
+
3
+ Python port of the Genesis2 Chip Generator (see
4
+ <https://github.com/StanfordVLSI/Genesis2>). Replaces the Perl runtime
5
+ with a pure-Python implementation while keeping the template hierarchy
6
+ (now `.vpy` / `.svpy`). Configuration is JSON; legacy Genesis2 `.xml`
7
+ configs convert via the bundled `genesispy-xml2json` helper.
8
+
9
+ ## Install
10
+
11
+ ### From PyPI
12
+
13
+ ```sh
14
+ pip install genesispy
15
+ ```
16
+
17
+ Installs the `genesispy`, `gvpy`, `genesispy-vp2vpy`, `genesispy-xml2json`,
18
+ `genesispy-json2xml` and `genesispy-jinja2j2` commands. There are no
19
+ mandatory dependencies; three optional extras:
20
+
21
+ ```sh
22
+ pip install 'genesispy[xml]' # lxml: pretty-printed genesispy-json2xml
23
+ pip install 'genesispy[color]' # colorama: coloured diagnostics
24
+ pip install 'genesispy[import-j2]' # jinja2: required by genesispy-jinja2j2
25
+ ```
26
+
27
+ ### From a checkout, without pip (recommended for developers -- `bin/` launchers)
28
+
29
+ The repo ships shell launchers in `bin/` (`bin/genesispy`, `bin/gvpy`)
30
+ that set `PYTHONPATH` to the sibling `src/` and exec
31
+ `python3 -m genesispy.cli`. No build step, no pip -- just run from a
32
+ checkout:
33
+
34
+ ```sh
35
+ ./bin/genesispy --input top.vpy --top top --json-cfg config.json
36
+ ```
37
+
38
+ To "install" to a destination, copy `bin/` and `src/` together (the
39
+ launchers resolve `src/` as `../src` relative to their own directory):
40
+
41
+ ```sh
42
+ DEST=/path/to/install-dir
43
+ mkdir -p "$DEST"
44
+ cp -a bin src "$DEST/"
45
+ export PATH="$DEST/bin:$PATH"
46
+ ```
47
+
48
+ ### From a checkout, with pip
49
+
50
+ ```sh
51
+ pip install -e .
52
+ # or
53
+ pip install --target /path/to/install-dir .
54
+ ```
55
+
56
+ ## Tests
57
+
58
+ Against the in-tree source, no install required:
59
+
60
+ ```sh
61
+ PYTHONPATH=src pytest tests/
62
+ ```
63
+
64
+ Against an installed copy (`pip install -e .` or `pip install genesispy`):
65
+
66
+ ```sh
67
+ pytest tests/
68
+ ```
69
+
70
+ ## Documentation
71
+
72
+ Each entry links to the rendered copy on GitHub; the `local` link resolves in a
73
+ source checkout.
74
+
75
+ - [doc/user-guide.md](https://github.com/stfns-s/genesispy/blob/main/doc/user-guide.md)
76
+ ([local](./doc/user-guide.md)) -- `.vpy` syntax, walkthrough, CLI reference
77
+ for `genesispy` and `gvpy`, migrating from Genesis2.
78
+ - [doc/code-structure.md](https://github.com/stfns-s/genesispy/blob/main/doc/code-structure.md)
79
+ ([local](./doc/code-structure.md)) -- pipeline, dedup, control flags.
80
+ - [doc/interfaces.md](https://github.com/stfns-s/genesispy/blob/main/doc/interfaces.md)
81
+ ([local](./doc/interfaces.md)) -- module-boundary interfaces.
82
+ - [doc/genesis2-incompatibilities.md](https://github.com/stfns-s/genesispy/blob/main/doc/genesis2-incompatibilities.md)
83
+ ([local](./doc/genesis2-incompatibilities.md)) -- non-obvious behavior
84
+ differences from Perl Genesis2.
@@ -0,0 +1,69 @@
1
+ [build-system]
2
+ # >=77 for PEP 639 (license = "<SPDX>" / license-files).
3
+ requires = ["setuptools>=77"]
4
+ build-backend = "setuptools.build_meta"
5
+
6
+ [project]
7
+ name = "genesispy"
8
+ version = "0.6.0"
9
+ description = "Genesis Chip Generator, Python port of Genesis2"
10
+ readme = "README.md"
11
+ requires-python = ">=3.12"
12
+ license = "Unlicense"
13
+ license-files = ["LICENSE"]
14
+ authors = [{ name = "stfns-s" }]
15
+ keywords = ["RTL generator", "verilog", "ASIC", "FPGA"]
16
+ classifiers = [
17
+ "Intended Audience :: Developers",
18
+ "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Operating System :: POSIX :: Linux",
22
+ ]
23
+ # No hard runtime requirements. Both accelerators below are guarded imports
24
+ # with stdlib fallbacks (tools/xml_json.py:23, reporting.py:21), so the
25
+ # package is fully functional without either.
26
+ dependencies = []
27
+
28
+ [project.optional-dependencies]
29
+ # Pretty-printed genesispy-json2xml output; without it the stdlib
30
+ # xml.etree.ElementTree writer is used.
31
+ xml = ["lxml>=4.9"]
32
+ # Coloured error/warning output; without it messages are uncoloured.
33
+ color = ["colorama>=0.4"]
34
+ dev = [
35
+ "pytest>=7",
36
+ "fixedpoint>=1.0",
37
+ "lxml>=4.9",
38
+ "colorama>=0.4",
39
+ ]
40
+ import-j2 = [
41
+ "jinja2>=3.1",
42
+ ]
43
+
44
+ [project.urls]
45
+ Homepage = "https://github.com/stfns-s/genesispy"
46
+ Repository = "https://github.com/stfns-s/genesispy"
47
+
48
+ [project.scripts]
49
+ genesispy = "genesispy.cli:main"
50
+ gvpy = "genesispy.gvpy_cli:main"
51
+ genesispy-jinja2j2 = "genesispy.tools.jinja2j2:main"
52
+ genesispy-xml2json = "genesispy.tools.xml_json:main_xml2json"
53
+ genesispy-json2xml = "genesispy.tools.xml_json:main_json2xml"
54
+ genesispy-vp2vpy = "genesispy.tools.vp2vpy:main"
55
+
56
+ [tool.setuptools.packages.find]
57
+ where = ["src"]
58
+
59
+ [tool.setuptools.package-dir]
60
+ "" = "src"
61
+
62
+ # vp2vpy locates its Perl helper as a sibling of vp2vpy.py
63
+ # (tools/vp2vpy.py:35), so it must ship inside the package.
64
+ [tool.setuptools.package-data]
65
+ "genesispy.tools" = ["*.pl"]
66
+
67
+ [tool.pytest.ini_options]
68
+ # demos/ carries the dsp-gpy submodule, which has a pytest suite of its own.
69
+ norecursedirs = ["demos", "build", "dist", ".git"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """genesispy — Python port of the Genesis2 Chip Generator."""
2
+
3
+ __version__ = "0.6.0"
@@ -0,0 +1,50 @@
1
+ """Shared scalar coercion used by config_handler and json_io.
2
+
3
+ Single source of truth for parsing string-shaped scalars (from XML, JSON,
4
+ or CLI ``-parameter NAME=VALUE``) into native Python ``int``/``float``/
5
+ ``bool``. Lives in its own leaf module so neither caller introduces an
6
+ import-time dependency on the other.
7
+
8
+ Behaviour: stricter than Python's ``float()`` — strings without a decimal
9
+ point or exponent character are not parsed as floats. This means
10
+ ``"inf"``, ``"nan"``, ``"infinity"`` round-trip as strings rather than
11
+ silently becoming ``float('inf')`` / ``float('nan')``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any
17
+
18
+
19
+ def coerce_scalar(s: Any) -> Any:
20
+ """Coerce a string scalar to int/float/bool when unambiguous.
21
+
22
+ Non-strings pass through. Empty / whitespace-only strings are kept
23
+ as the original string. Recognised:
24
+ * ``"true"`` / ``"false"`` (case-insensitive) -> ``bool``.
25
+ * Optional sign + digits -> ``int``.
26
+ * Strings containing ``.``, ``e``, or ``E`` that ``float()``
27
+ accepts -> ``float``.
28
+ Anything else returns the input unchanged.
29
+ """
30
+ if not isinstance(s, str):
31
+ return s
32
+ stripped = s.strip()
33
+ if stripped == "":
34
+ return s
35
+ low = stripped.lower()
36
+ if low == "true":
37
+ return True
38
+ if low == "false":
39
+ return False
40
+ if stripped.lstrip("+-").isdigit():
41
+ try:
42
+ return int(stripped)
43
+ except ValueError:
44
+ pass
45
+ if any(c in stripped for c in ".eE"):
46
+ try:
47
+ return float(stripped)
48
+ except ValueError:
49
+ pass
50
+ return s
@@ -0,0 +1,208 @@
1
+ """Process-wide singletons backing the elaboration engine.
2
+
3
+ These dictionaries replace the Perl ``shared-ref`` globals used in
4
+ ``UniqueModule.pm`` (see e.g. lines 176-181, 248-251 of that file). They
5
+ are intentionally module-level so that every ``UniqueModule`` instance
6
+ agrees on the dedup state. Tests reset them via :func:`clear_all`.
7
+
8
+ The two journaled caches (MODULE_CACHE, OUTFILE_CONTENT_CACHE) record
9
+ writes inside an active :func:`journaled` block so :meth:`UniqueModule.unique_inst`
10
+ can roll back the discarded child's registrations on a post-elaboration
11
+ dedup hit without paying O(N) to copy/restore the entire cache.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from contextlib import contextmanager
17
+ from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Tuple
18
+
19
+ if TYPE_CHECKING: # pragma: no cover
20
+ from .unique_module import UniqueModule
21
+
22
+
23
+ _MISSING = object()
24
+
25
+
26
+ class _JournaledDict(dict):
27
+ """``dict`` subclass that records first-touch writes for active journals.
28
+
29
+ Each entry on :attr:`_journals` is a ``dict[key -> pre_value]`` capturing
30
+ what the key was *before* the first write inside that journal scope; the
31
+ sentinel :data:`_MISSING` means the key was absent. Rollback walks the
32
+ journal and restores or deletes accordingly.
33
+
34
+ Only ``__setitem__`` and ``__delitem__`` are journaled; ``.clear()`` is
35
+ used only by :func:`clear_all` between tests and bypasses journaling on
36
+ purpose. No call site uses ``.update()`` / ``.pop()`` / ``.popitem()``
37
+ while a journal is active (verified at port time); add overrides if that
38
+ changes.
39
+ """
40
+
41
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
42
+ super().__init__(*args, **kwargs)
43
+ self._journals: List[Dict[Any, Any]] = []
44
+
45
+ def _record(self, key: Any) -> None:
46
+ if not self._journals:
47
+ return
48
+ prev = super().get(key, _MISSING)
49
+ for j in self._journals:
50
+ if key not in j:
51
+ j[key] = prev
52
+
53
+ def __setitem__(self, key: Any, value: Any) -> None:
54
+ self._record(key)
55
+ super().__setitem__(key, value)
56
+
57
+ def __delitem__(self, key: Any) -> None:
58
+ self._record(key)
59
+ super().__delitem__(key)
60
+
61
+
62
+ # MODULE_CACHE keys live in two disjoint namespaces, partitioned by the
63
+ # `::` separator:
64
+ # * dedup signatures used by unique_inst / unique_inst_param to collapse
65
+ # equivalent elaborations:
66
+ # "<base>::<sha256>[::sub::<sha256>]" (pre-elaboration param key)
67
+ # "<base>::post::<sha256>[::sub::<sha256>]" (post-elaboration full-param key)
68
+ # "<base>::param::<sha256>[::sub::<sha256>]" (parametric form)
69
+ # The optional `::sub::` tail (unique_module._subtree_tag) folds the
70
+ # scoped-subtree override signature into the key, separating instances
71
+ # whose descendants carry different scoped CLI overrides.
72
+ # * registered instance identifiers (`<base>_unqN`, plus user-supplied
73
+ # synonyms) — these must NOT contain `::`; cache.register asserts this
74
+ # so a future synonym name can never collide with a dedup signature.
75
+ MODULE_CACHE: _JournaledDict = _JournaledDict()
76
+
77
+ # Base-class-name -> next derivative counter. Drives unique-name suffixes
78
+ # such as ``Foo_unq2`` (unique_inst, plus unique_inst_param on override
79
+ # paths). Namespaced keys like ``<base>::ununq_tmp`` number the temp
80
+ # generations of ununique_inst's re-elaborate-and-compare path.
81
+ MODULE_NAME_NUM_DERIVS: Dict[str, int] = {}
82
+
83
+ # Filename -> emitted Verilog text. Flushed on demand (e.g. by Manager).
84
+ OUTFILE_CONTENT_CACHE: _JournaledDict = _JournaledDict()
85
+
86
+ # Base-name -> {"instance": UniqueModule, "params": dict[str, Any],
87
+ # "subtree_sig": tuple}. Tracks `ununique_inst` calls; a second call with
88
+ # the same base name aliases the previous instance (identical resolved
89
+ # params and subtree signature), raises (different params), or
90
+ # re-elaborates and compares generated bodies (different subtree
91
+ # signatures). Mirrors Perl UnUniquifiedModules + does_generate_same +
92
+ # compare_generated_files (UniqueModule.pm:1610-1700, :3130); global
93
+ # scope (not per-parent) because the on-disk filename is global.
94
+ UNUNIQUE_REGISTRY: Dict[str, Dict[str, Any]] = {}
95
+
96
+
97
+ # Filename -> 'synth' | 'verif' | 'synth_and_verif'. Built by Manager
98
+ # before flush from a path-based DFS over the elaborated instance tree
99
+ # (mirrors Perl Manager.pm:1330-1395 / UniqueModule.pm:_get_prod_list_insts).
100
+ # Empty when synth_top is None -> output_writer treats unmapped files as
101
+ # 'verif' (matches Perl SynthTop=undef default).
102
+ OUTFILE_TAGS: Dict[str, str] = {}
103
+
104
+ # Output filenames in DFS first-seen walk order, populated by Manager
105
+ # _populate_outfile_tags alongside OUTFILE_TAGS. output_writer uses this
106
+ # to emit product lists in a single consistent order (matching Perl
107
+ # Manager.pm:1330-1395). Keys not present here (test-only raw entries)
108
+ # fall back to alphabetical after all ordered entries.
109
+ OUTFILE_ORDER: List[str] = []
110
+
111
+ # Resolved paths of include()'d template files, appended by
112
+ # user_config._include. Consumed together with Manager.parsed_source_files
113
+ # by output_writer.write_file_lists as the .depend prerequisite list.
114
+ # Append order; deduped at read time.
115
+ INCLUDED_FILES: List[str] = []
116
+
117
+
118
+ def clear_all() -> None:
119
+ """Reset every singleton. Intended for tests."""
120
+ MODULE_CACHE.clear()
121
+ MODULE_NAME_NUM_DERIVS.clear()
122
+ OUTFILE_CONTENT_CACHE.clear()
123
+ OUTFILE_TAGS.clear()
124
+ OUTFILE_ORDER.clear()
125
+ UNUNIQUE_REGISTRY.clear()
126
+ INCLUDED_FILES.clear()
127
+ # Recycled tmpdir paths could otherwise inherit a stale .vpy mapping.
128
+ from .template import runtime as _rt
129
+ _rt.clear_line_maps()
130
+
131
+
132
+ def next_derivation(base_name: str) -> int:
133
+ """Return the next derivative index for ``base_name`` (1-based).
134
+
135
+ The first call returns ``1``; subsequent calls increment. This
136
+ matches the Perl ``ModuleNameNumDerivs`` semantics.
137
+
138
+ Best-effort contiguous: gaps may appear when post-elaboration dedup
139
+ in ``unique_inst`` reclaims a slot, and the rollback only fires if
140
+ no nested ``next_derivation`` call bumped the counter past it.
141
+ """
142
+ n = MODULE_NAME_NUM_DERIVS.get(base_name, 0) + 1
143
+ MODULE_NAME_NUM_DERIVS[base_name] = n
144
+ return n
145
+
146
+
147
+ def register(unique_name: str, instance: "UniqueModule") -> None:
148
+ """Register ``instance`` under ``unique_name`` in the module cache.
149
+
150
+ Re-registering the same instance is a silent no-op. Re-registering a
151
+ *different* instance under an existing name emits a one-line warning
152
+ on stderr — typically a synonym collision or a misuse of
153
+ `synonym_class`. The new entry still wins (preserves prior behaviour
154
+ for tests that intentionally rebind), but the warning surfaces what
155
+ used to be a silent overwrite.
156
+ """
157
+ if "::" in unique_name:
158
+ # Reserved for dedup-signature keys; see module docstring.
159
+ raise ValueError(
160
+ f"cache.register: '::' is reserved in unique-name keys; "
161
+ f"got {unique_name!r}"
162
+ )
163
+ existing = MODULE_CACHE.get(unique_name)
164
+ if existing is not None and existing is not instance:
165
+ from . import reporting
166
+
167
+ reporting.warning(
168
+ f"cache.register: {unique_name!r} already bound to a different "
169
+ f"UniqueModule instance; overwriting."
170
+ )
171
+ MODULE_CACHE[unique_name] = instance
172
+
173
+
174
+ @contextmanager
175
+ def journaled() -> Iterator[Tuple[Dict[Any, Any], Dict[Any, Any]]]:
176
+ """Context manager: capture writes to MODULE_CACHE and OUTFILE_CONTENT_CACHE.
177
+
178
+ Yields a ``(mc_journal, oc_journal)`` pair of dicts that map each
179
+ touched key to its pre-block value (or :data:`_MISSING` if absent at
180
+ block entry). Pass these to :func:`rollback_journal` to undo only the
181
+ writes recorded inside the block, leaving unrelated entries untouched.
182
+ Journals nest: each scope tracks its own first-touch set.
183
+ """
184
+ mc_j: Dict[Any, Any] = {}
185
+ oc_j: Dict[Any, Any] = {}
186
+ MODULE_CACHE._journals.append(mc_j)
187
+ OUTFILE_CONTENT_CACHE._journals.append(oc_j)
188
+ try:
189
+ yield mc_j, oc_j
190
+ finally:
191
+ OUTFILE_CONTENT_CACHE._journals.pop()
192
+ MODULE_CACHE._journals.pop()
193
+
194
+
195
+ def rollback_journal(
196
+ mc_j: Dict[Any, Any], oc_j: Dict[Any, Any]
197
+ ) -> None:
198
+ """Undo writes recorded by a :func:`journaled` block on both caches."""
199
+ for key, prev in mc_j.items():
200
+ if prev is _MISSING:
201
+ dict.pop(MODULE_CACHE, key, None)
202
+ else:
203
+ dict.__setitem__(MODULE_CACHE, key, prev)
204
+ for key, prev in oc_j.items():
205
+ if prev is _MISSING:
206
+ dict.pop(OUTFILE_CONTENT_CACHE, key, None)
207
+ else:
208
+ dict.__setitem__(OUTFILE_CONTENT_CACHE, key, prev)