genesispy 0.6.0__py3-none-any.whl

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.
genesispy/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """genesispy — Python port of the Genesis2 Chip Generator."""
2
+
3
+ __version__ = "0.6.0"
genesispy/_scalars.py ADDED
@@ -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
genesispy/cache.py ADDED
@@ -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)