[BuildLibrary] allow prefixing/renaming/postprocessing in BuildCellsView
This commit is contained in:
parent
b62879b410
commit
890e7e5c0f
3 changed files with 611 additions and 40 deletions
|
|
@ -45,6 +45,16 @@ def main() -> None:
|
|||
builder = LibraryBuilder()
|
||||
cells = builder.cells
|
||||
|
||||
# Naming and export policy can be bound once without changing individual
|
||||
# declarations. For example, a foundry-specific group could use:
|
||||
#
|
||||
# cells = builder.cells.prefixed(
|
||||
# 'foundry_',
|
||||
# facade=lambda proxy: devices.ports_to_data(proxy),
|
||||
# )
|
||||
#
|
||||
# Dynamic names use the same view: cells[generated_name] = cell(factory)(...)
|
||||
|
||||
#
|
||||
# Load some devices from a GDS file
|
||||
#
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, replace
|
||||
from functools import wraps
|
||||
from types import MappingProxyType
|
||||
|
|
@ -16,7 +17,7 @@ from .mapping import Library, LibraryView
|
|||
from .overlay import OverlayLibrary
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterator, KeysView, Mapping, Sequence
|
||||
from collections.abc import Callable, Iterator, KeysView, Sequence
|
||||
|
||||
from ..pattern import Pattern
|
||||
|
||||
|
|
@ -76,8 +77,8 @@ class BuildReport:
|
|||
|
||||
@dataclass
|
||||
class _BuildRecipe:
|
||||
""" Captured deferred call to a pattern factory. """
|
||||
func: Callable[..., Pattern]
|
||||
""" Captured deferred call to a pattern or tree factory. """
|
||||
func: Callable[..., Pattern | TreeView]
|
||||
args: tuple[Any, ...]
|
||||
kwargs: dict[str, Any]
|
||||
explicit_dependencies: tuple[str, ...] = ()
|
||||
|
|
@ -87,9 +88,16 @@ class _BuildRecipe:
|
|||
return self
|
||||
|
||||
|
||||
def cell(func: Callable[..., Pattern]) -> Callable[..., _BuildRecipe]:
|
||||
@dataclass(frozen=True)
|
||||
class _BuildDeclaration:
|
||||
"""One deferred or direct declaration plus its implementation transform."""
|
||||
value: Pattern | TreeView | _BuildRecipe
|
||||
postprocess: Callable[[Pattern], Pattern] | None = None
|
||||
|
||||
|
||||
def cell(func: Callable[..., Pattern | TreeView]) -> Callable[..., _BuildRecipe]:
|
||||
"""
|
||||
Wrap a plain pattern factory so calls return deferred build recipes.
|
||||
Wrap a plain Pattern or single-top tree factory so calls return deferred build recipes.
|
||||
|
||||
Use as either `cell(fn)(...)` or `@cell`.
|
||||
"""
|
||||
|
|
@ -111,19 +119,138 @@ class _LibraryPlaceholder:
|
|||
return '<LibraryBuilder.library>'
|
||||
|
||||
|
||||
def _identity_name(name: str) -> str:
|
||||
return name
|
||||
|
||||
|
||||
def _make_facade(
|
||||
library: ILibrary,
|
||||
target: str,
|
||||
transform: Callable[[Pattern], Pattern],
|
||||
) -> Pattern:
|
||||
from ..pattern import Pattern # noqa: PLC0415
|
||||
|
||||
implementation = library[target]
|
||||
proxy = Pattern(ports=implementation.ports).ref(target)
|
||||
result = transform(proxy)
|
||||
if not isinstance(result, Pattern):
|
||||
raise BuildError(f'Facade transform returned {type(result).__name__}, expected Pattern')
|
||||
return result
|
||||
|
||||
|
||||
class BuildCellsView:
|
||||
"""
|
||||
Attribute-based declaration namespace for `LibraryBuilder`.
|
||||
|
||||
This is the ergonomic authoring surface exposed as `builder.cells`. It is
|
||||
intentionally write-focused: attribute assignment and deletion register
|
||||
declarations, while attribute reads fail with guidance to build first and
|
||||
intentionally write-focused: attribute or item assignment and deletion
|
||||
register declarations, while reads fail with guidance to build first and
|
||||
use the returned library.
|
||||
"""
|
||||
__slots__ = ('_library',)
|
||||
|
||||
def __init__(self, library: LibraryBuilder) -> None:
|
||||
`prefixed()` and `renamed()` create independent write-through views over
|
||||
the same builder. Their optional `postprocess` callable transforms the
|
||||
implementation Pattern. If a name changes, their optional `facade`
|
||||
callable transforms a generated Pattern with copied ports and a reference
|
||||
to the implementation.
|
||||
|
||||
Example:
|
||||
`cells = builder.cells.prefixed(prefix, facade=encode_ports)`
|
||||
`cells.device = cell(make_device)(width=10)`
|
||||
`cells[generated_name] = cell(make_device)(width=20)`
|
||||
"""
|
||||
__slots__ = ('_facade', '_library', '_postprocess', '_rename')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: LibraryBuilder,
|
||||
*,
|
||||
rename: Callable[[str], str] = _identity_name,
|
||||
facade: Callable[[Pattern], Pattern] | None = None,
|
||||
postprocess: Callable[[Pattern], Pattern] | None = None,
|
||||
) -> None:
|
||||
object.__setattr__(self, '_library', library)
|
||||
object.__setattr__(self, '_rename', rename)
|
||||
object.__setattr__(self, '_facade', facade)
|
||||
object.__setattr__(self, '_postprocess', postprocess)
|
||||
|
||||
def prefixed(
|
||||
self,
|
||||
prefix: str | None,
|
||||
*,
|
||||
facade: Callable[[Pattern], Pattern] | None = None,
|
||||
postprocess: Callable[[Pattern], Pattern] | None = None,
|
||||
) -> BuildCellsView:
|
||||
"""
|
||||
Return an independent write-through view which conditionally prefixes names.
|
||||
|
||||
Names which already start with a non-empty `prefix` are left unchanged.
|
||||
`None` and the empty string select the identity naming policy.
|
||||
|
||||
Args:
|
||||
prefix: Prefix for implementation names.
|
||||
facade: Transform applied to a generated logical-name proxy when
|
||||
prefixing changes a name.
|
||||
postprocess: Transform applied to the implementation Pattern on
|
||||
each validation or build.
|
||||
"""
|
||||
if prefix is not None and not isinstance(prefix, str):
|
||||
raise TypeError('Build cell prefixes must be strings or None.')
|
||||
self._validate_transform('facade', facade)
|
||||
self._validate_transform('postprocess', postprocess)
|
||||
|
||||
def add_prefix(name: str) -> str:
|
||||
if not prefix or name.startswith(prefix):
|
||||
return name
|
||||
return prefix + name
|
||||
|
||||
return BuildCellsView(
|
||||
self._library,
|
||||
rename=add_prefix,
|
||||
facade=facade,
|
||||
postprocess=postprocess,
|
||||
)
|
||||
|
||||
def renamed(
|
||||
self,
|
||||
rename: Callable[[str], str],
|
||||
*,
|
||||
facade: Callable[[Pattern], Pattern] | None = None,
|
||||
postprocess: Callable[[Pattern], Pattern] | None = None,
|
||||
) -> BuildCellsView:
|
||||
"""
|
||||
Return an independent write-through view using `rename(name)` for output names.
|
||||
|
||||
`rename` must be deterministic and return a string. `facade` and
|
||||
`postprocess` have the same behavior as in `prefixed()`.
|
||||
"""
|
||||
if not callable(rename):
|
||||
raise TypeError('Build cell name transforms must be callable.')
|
||||
self._validate_transform('facade', facade)
|
||||
self._validate_transform('postprocess', postprocess)
|
||||
return BuildCellsView(
|
||||
self._library,
|
||||
rename=rename,
|
||||
facade=facade,
|
||||
postprocess=postprocess,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _validate_transform(
|
||||
description: str,
|
||||
transform: Callable[[Pattern], Pattern] | None,
|
||||
) -> None:
|
||||
if transform is not None and not callable(transform):
|
||||
raise TypeError(f'Build cell {description} transforms must be callable or None.')
|
||||
|
||||
def _physical_name(self, name: str) -> str:
|
||||
if not isinstance(name, str):
|
||||
raise TypeError('Build cell names must be strings.')
|
||||
physical_name = self._rename(name)
|
||||
if not isinstance(physical_name, str):
|
||||
raise TypeError(
|
||||
f'Build cell name transform returned {type(physical_name).__name__}, expected str.'
|
||||
)
|
||||
return physical_name
|
||||
|
||||
def __getattr__(self, name: str) -> Pattern:
|
||||
raise BuildError(
|
||||
|
|
@ -131,16 +258,47 @@ class BuildCellsView:
|
|||
'Call build() and index the returned library instead.'
|
||||
)
|
||||
|
||||
def __setattr__(self, name: str, value: Pattern | _BuildRecipe) -> None:
|
||||
if name == '_library':
|
||||
object.__setattr__(self, name, value)
|
||||
return
|
||||
self._library[name] = value
|
||||
def __getitem__(self, name: str) -> Pattern:
|
||||
raise BuildError(
|
||||
f'LibraryBuilder.cells[{name!r}] is write-only during authoring. '
|
||||
'Call build() and index the returned library instead.'
|
||||
)
|
||||
|
||||
def __setitem__(
|
||||
self,
|
||||
name: str,
|
||||
value: Pattern | TreeView | _BuildRecipe,
|
||||
) -> None:
|
||||
self._library._assert_editable()
|
||||
physical_name = self._physical_name(name)
|
||||
self._library._register_view_declaration(
|
||||
logical_name=name,
|
||||
physical_name=physical_name,
|
||||
value=value,
|
||||
facade=self._facade,
|
||||
postprocess=self._postprocess,
|
||||
)
|
||||
|
||||
def __delitem__(self, name: str) -> None:
|
||||
self._library._assert_editable()
|
||||
physical_name = self._physical_name(name)
|
||||
if physical_name != name:
|
||||
emitted = [physical_name]
|
||||
if self._facade is not None:
|
||||
emitted.append(name)
|
||||
raise BuildError(
|
||||
f'Cannot delete renamed build declaration {name!r} through this view; '
|
||||
f'it emitted {emitted}. Delete concrete names through builder.cells[...] instead.'
|
||||
)
|
||||
del self._library[name]
|
||||
|
||||
def __setattr__(self, name: str, value: Pattern | TreeView | _BuildRecipe) -> None:
|
||||
self[name] = value
|
||||
|
||||
def __delattr__(self, name: str) -> None:
|
||||
if name == '_library':
|
||||
if name in self.__slots__:
|
||||
raise AttributeError(name)
|
||||
del self._library[name]
|
||||
del self[name]
|
||||
|
||||
|
||||
class LibraryBuilder(INameView):
|
||||
|
|
@ -148,8 +306,8 @@ class LibraryBuilder(INameView):
|
|||
Two-phase declaration surface for mixed imported/generated libraries.
|
||||
|
||||
A `LibraryBuilder` collects three kinds of inputs:
|
||||
- direct declared `Pattern` objects
|
||||
- deferred recipes created with `cell(...)`
|
||||
- direct declared `Pattern` objects or single-top trees
|
||||
- deferred Pattern or tree recipes created with `cell(...)`
|
||||
- imported source-backed library views added with `add_source(...)`
|
||||
|
||||
The builder itself is not a normal readable library during authoring.
|
||||
|
|
@ -164,7 +322,7 @@ class LibraryBuilder(INameView):
|
|||
self._library_placeholder = _LibraryPlaceholder(self)
|
||||
self._frozen = False
|
||||
self._building = False
|
||||
self._declarations: dict[str, Pattern | _BuildRecipe] = {}
|
||||
self._declarations: dict[str, _BuildDeclaration] = {}
|
||||
self._sources: list[tuple[ILibraryView, dict[str, str]]] = []
|
||||
self._names: dict[str, None] = {}
|
||||
|
||||
|
|
@ -194,11 +352,16 @@ class LibraryBuilder(INameView):
|
|||
def __setitem__(
|
||||
self,
|
||||
key: str,
|
||||
value: Pattern | _BuildRecipe,
|
||||
value: Pattern | TreeView | _BuildRecipe,
|
||||
) -> None:
|
||||
self._assert_editable()
|
||||
if key in self._names:
|
||||
raise LibraryError(f'"{key}" already exists in the builder. Overwriting is not allowed!')
|
||||
self._register_declarations(((key, value, None),))
|
||||
|
||||
def _prepare_declaration(
|
||||
self,
|
||||
value: Pattern | TreeView | _BuildRecipe,
|
||||
postprocess: Callable[[Pattern], Pattern] | None,
|
||||
) -> _BuildDeclaration:
|
||||
from ..pattern import Pattern # noqa: PLC0415
|
||||
|
||||
if isinstance(value, _BuildRecipe):
|
||||
placeholders = (
|
||||
|
|
@ -207,14 +370,57 @@ class LibraryBuilder(INameView):
|
|||
)
|
||||
if any(placeholder is not self.library for placeholder in placeholders):
|
||||
raise BuildError('A recipe cannot use another LibraryBuilder.library placeholder.')
|
||||
declaration = value
|
||||
else:
|
||||
if callable(value):
|
||||
elif callable(value):
|
||||
raise TypeError('LibraryBuilder recipes must be wrapped with cell(fn)(...) or @cell.')
|
||||
declaration = value
|
||||
elif not isinstance(value, Pattern | Mapping):
|
||||
raise TypeError(
|
||||
f'LibraryBuilder declarations must be a Pattern, tree, or cell recipe, not {type(value).__name__}.'
|
||||
)
|
||||
return _BuildDeclaration(value=value, postprocess=postprocess)
|
||||
|
||||
self._declarations[key] = declaration
|
||||
self._names[key] = None
|
||||
def _register_declarations(
|
||||
self,
|
||||
entries: Sequence[tuple[str, Pattern | TreeView | _BuildRecipe, Callable[[Pattern], Pattern] | None]],
|
||||
) -> None:
|
||||
self._assert_editable()
|
||||
entry_names = [name for name, _value, _postprocess in entries]
|
||||
if any(not isinstance(name, str) for name in entry_names):
|
||||
raise TypeError('Build cell names must be strings.')
|
||||
if len(set(entry_names)) != len(entry_names):
|
||||
raise LibraryError(f'Duplicate names in build declaration: {entry_names}')
|
||||
conflicts = [name for name in entry_names if name in self._names]
|
||||
if conflicts:
|
||||
raise LibraryError(f'Build declaration names already exist: {conflicts}')
|
||||
|
||||
prepared = [
|
||||
(name, self._prepare_declaration(value, postprocess))
|
||||
for name, value, postprocess in entries
|
||||
]
|
||||
for name, declaration in prepared:
|
||||
self._declarations[name] = declaration
|
||||
self._names[name] = None
|
||||
|
||||
def _register_view_declaration(
|
||||
self,
|
||||
*,
|
||||
logical_name: str,
|
||||
physical_name: str,
|
||||
value: Pattern | TreeView | _BuildRecipe,
|
||||
facade: Callable[[Pattern], Pattern] | None,
|
||||
postprocess: Callable[[Pattern], Pattern] | None,
|
||||
) -> None:
|
||||
entries: list[tuple[str, Pattern | TreeView | _BuildRecipe, Callable[[Pattern], Pattern] | None]] = [
|
||||
(physical_name, value, postprocess),
|
||||
]
|
||||
if physical_name != logical_name and facade is not None:
|
||||
facade_recipe = _BuildRecipe(
|
||||
func=_make_facade,
|
||||
args=(self.library, physical_name, facade),
|
||||
kwargs={},
|
||||
explicit_dependencies=(physical_name,),
|
||||
)
|
||||
entries.append((logical_name, facade_recipe, None))
|
||||
self._register_declarations(entries)
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
self._assert_editable()
|
||||
|
|
@ -431,6 +637,23 @@ class LibraryBuilder(INameView):
|
|||
return session, report
|
||||
|
||||
|
||||
class _NamesWithout(INameView):
|
||||
"""Name view which temporarily makes one reserved declaration available."""
|
||||
|
||||
def __init__(self, names: INameView, omitted: str) -> None:
|
||||
self._names = names
|
||||
self._omitted = omitted
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return (name for name in self._names if name != self._omitted)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._names) - int(self._omitted in self._names)
|
||||
|
||||
def __contains__(self, key: object) -> bool:
|
||||
return key != self._omitted and key in self._names
|
||||
|
||||
|
||||
class _BuildSessionLibrary(ILibrary):
|
||||
"""
|
||||
Internal overlay-backed library used while a `LibraryBuilder` is executing.
|
||||
|
|
@ -629,6 +852,75 @@ class _BuildSessionLibrary(ILibrary):
|
|||
return
|
||||
raise BuildError(f'Missing dependency "{name}"')
|
||||
|
||||
def _apply_postprocess(
|
||||
self,
|
||||
name: str,
|
||||
pattern: Pattern,
|
||||
postprocess: Callable[[Pattern], Pattern] | None,
|
||||
) -> Pattern:
|
||||
from ..pattern import Pattern # noqa: PLC0415
|
||||
|
||||
if postprocess is None:
|
||||
return pattern
|
||||
result = postprocess(pattern)
|
||||
if not isinstance(result, Pattern):
|
||||
raise BuildError(
|
||||
f'Postprocess transform for "{name}" returned {type(result).__name__}, expected Pattern'
|
||||
)
|
||||
return result
|
||||
|
||||
def _commit_declared_tree(
|
||||
self,
|
||||
name: str,
|
||||
tree: TreeView,
|
||||
postprocess: Callable[[Pattern], Pattern] | None,
|
||||
) -> Pattern:
|
||||
from ..pattern import map_targets # noqa: PLC0415
|
||||
|
||||
view = tree if isinstance(tree, ILibraryView) else LibraryView(tree)
|
||||
source_order = tuple(view.source_order())
|
||||
temp = Library(copy.deepcopy({source_name: view[source_name] for source_name in source_order}))
|
||||
tops = temp.tops()
|
||||
if len(tops) != 1:
|
||||
raise BuildError(f'Tree declaration for "{name}" must have exactly one topcell, found {tops}')
|
||||
|
||||
old_top = tops[0]
|
||||
source_order = (old_top, *(source_name for source_name in temp if source_name != old_top))
|
||||
|
||||
def rename_tree_source(names: INameView, source_name: str) -> str:
|
||||
if source_name == old_top:
|
||||
return name
|
||||
if source_name in names:
|
||||
return _rename_patterns(names, source_name)
|
||||
return source_name
|
||||
|
||||
source_to_visible = _plan_source_names(
|
||||
_NamesWithout(self, name),
|
||||
source_order,
|
||||
rename_theirs=rename_tree_source,
|
||||
rename_when='always',
|
||||
)
|
||||
temp.mapping[old_top] = self._apply_postprocess(name, temp[old_top], postprocess)
|
||||
rename_map = _source_rename_map(source_to_visible)
|
||||
if rename_map:
|
||||
target_map = cast('dict[str | None, str | None]', rename_map)
|
||||
remapped_refs = {
|
||||
source_name: map_targets(temp[source_name].refs, lambda target: target_map.get(target, target))
|
||||
for source_name in source_order
|
||||
}
|
||||
for source_name, refs in remapped_refs.items():
|
||||
temp[source_name].refs = refs
|
||||
|
||||
for source_name in source_order:
|
||||
visible_name = source_to_visible[source_name]
|
||||
self[visible_name] = temp[source_name]
|
||||
if source_name not in (old_top, visible_name):
|
||||
self._provenance[visible_name] = replace(
|
||||
self._provenance[visible_name],
|
||||
requested_name=source_name,
|
||||
)
|
||||
return temp[old_top]
|
||||
|
||||
def _ensure_declared(self, name: str) -> None:
|
||||
from ..pattern import Pattern # noqa: PLC0415
|
||||
|
||||
|
|
@ -639,30 +931,44 @@ class _BuildSessionLibrary(ILibrary):
|
|||
raise BuildError(f'Cycle detected while building declared cells: {chain}')
|
||||
|
||||
declaration = self._builder._declarations[name]
|
||||
value = declaration.value
|
||||
self._declared_stack.append(name)
|
||||
try:
|
||||
if isinstance(declaration, _BuildRecipe):
|
||||
for dep in declaration.explicit_dependencies:
|
||||
if isinstance(value, _BuildRecipe):
|
||||
for dep in value.explicit_dependencies:
|
||||
self._ensure_named(dep)
|
||||
args = tuple(
|
||||
self if arg is self._builder.library else arg
|
||||
for arg in declaration.args
|
||||
for arg in value.args
|
||||
)
|
||||
kwargs = {
|
||||
key: self if value is self._builder.library else value
|
||||
for key, value in declaration.kwargs.items()
|
||||
key: self if arg_value is self._builder.library else arg_value
|
||||
for key, arg_value in value.kwargs.items()
|
||||
}
|
||||
pattern = declaration.func(*args, **kwargs)
|
||||
if not isinstance(pattern, Pattern):
|
||||
raise BuildError(f'Recipe for "{name}" returned {type(pattern).__name__}, expected Pattern') # noqa: TRY301
|
||||
result = value.func(*args, **kwargs)
|
||||
else:
|
||||
pattern = declaration.deepcopy()
|
||||
result = value.deepcopy() if isinstance(value, Pattern) else value
|
||||
|
||||
if isinstance(result, Pattern):
|
||||
published_pattern = result
|
||||
pattern = self._apply_postprocess(name, result, declaration.postprocess)
|
||||
elif isinstance(result, Mapping):
|
||||
pattern = self._commit_declared_tree(name, result, declaration.postprocess)
|
||||
published_pattern = pattern
|
||||
else:
|
||||
raise BuildError( # noqa: TRY301
|
||||
f'Recipe for "{name}" returned {type(result).__name__}, expected Pattern or single-top tree'
|
||||
)
|
||||
|
||||
if name in self._overlay:
|
||||
if self._overlay[name] is not pattern:
|
||||
existing = self._overlay[name]
|
||||
if existing is not published_pattern:
|
||||
raise BuildError( # noqa: TRY301
|
||||
f'Recipe for "{name}" wrote a different pattern into the session under its own name.'
|
||||
)
|
||||
if existing is not pattern:
|
||||
del self._overlay[name]
|
||||
self._overlay[name] = pattern
|
||||
else:
|
||||
self[name] = pattern
|
||||
self._built.add(name)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from ..library import (
|
|||
)
|
||||
from ..pattern import Pattern
|
||||
from ..ports import Port
|
||||
from ..utils.ports2data import ports_to_data
|
||||
|
||||
|
||||
def _owned_by(report: BuildReport, owner: str) -> set[str]:
|
||||
|
|
@ -832,3 +833,257 @@ def test_build_session_subtree_preserves_type_and_builds_deferred_dependencies()
|
|||
|
||||
assert child_calls == 1
|
||||
assert set(built) == {"top", "child", "leaf", "unrelated"}
|
||||
|
||||
|
||||
def test_build_cells_prefixed_view_generates_facades_for_attributes_and_items() -> None:
|
||||
builder = LibraryBuilder()
|
||||
cells = builder.cells.prefixed(
|
||||
"vendor_",
|
||||
facade=lambda proxy: ports_to_data(proxy, (10, 2)),
|
||||
)
|
||||
|
||||
cells.fixed = cell(Pattern)(ports={"p": Port((1, 2), 0)})
|
||||
dynamic_name = "dynamic"
|
||||
cells[dynamic_name] = cell(Pattern)(ports={"q": Port((3, 4), 0)})
|
||||
built, report = builder.build(output="library")
|
||||
|
||||
assert set(built) == {"vendor_fixed", "fixed", "vendor_dynamic", "dynamic"}
|
||||
assert set(built["fixed"].refs) == {"vendor_fixed"}
|
||||
assert set(built["dynamic"].refs) == {"vendor_dynamic"}
|
||||
assert set(built["fixed"].ports) == {"p"}
|
||||
assert len(built["fixed"].labels[(10, 2)]) == 1
|
||||
assert not built["vendor_fixed"].labels
|
||||
assert report.dependency_graph["fixed"] == frozenset({"vendor_fixed"})
|
||||
assert report.dependency_graph["dynamic"] == frozenset({"vendor_dynamic"})
|
||||
|
||||
|
||||
def test_build_cells_prefixed_view_postprocesses_only_the_implementation() -> None:
|
||||
source = Pattern(ports={"p": Port((0, 0), 0)})
|
||||
builder = LibraryBuilder()
|
||||
physical = builder.cells.prefixed(
|
||||
"vendor_",
|
||||
postprocess=lambda pattern: ports_to_data(pattern, (20, 1)),
|
||||
)
|
||||
physical["cell"] = source
|
||||
unprefixed = builder.cells.prefixed(
|
||||
None,
|
||||
postprocess=lambda pattern: ports_to_data(pattern, (21, 1)),
|
||||
)
|
||||
unprefixed["local"] = Pattern(ports={"q": Port((1, 1), 0)})
|
||||
built, _report = builder.build(output="library")
|
||||
|
||||
assert set(built) == {"vendor_cell", "local"}
|
||||
assert len(built["vendor_cell"].labels[(20, 1)]) == 1
|
||||
assert len(built["local"].labels[(21, 1)]) == 1
|
||||
assert not source.labels
|
||||
|
||||
|
||||
def test_build_cells_postprocess_precedes_facade_port_copy() -> None:
|
||||
builder = LibraryBuilder()
|
||||
|
||||
def add_port(pattern: Pattern) -> Pattern:
|
||||
pattern.ports["added"] = Port((5, 6), 0)
|
||||
return pattern
|
||||
|
||||
cells = builder.cells.prefixed(
|
||||
"impl_",
|
||||
postprocess=add_port,
|
||||
facade=lambda pattern: ports_to_data(pattern, (30, 0)),
|
||||
)
|
||||
cells.device = Pattern()
|
||||
built, _report = builder.build(output="library")
|
||||
|
||||
assert set(built["impl_device"].ports) == {"added"}
|
||||
assert set(built["device"].ports) == {"added"}
|
||||
assert built["device"].labels[(30, 0)][0].string.startswith("added:")
|
||||
|
||||
|
||||
def test_build_cells_facade_is_ignored_when_name_does_not_change() -> None:
|
||||
builder = LibraryBuilder()
|
||||
|
||||
def fail_if_called(_pattern: Pattern) -> Pattern:
|
||||
raise AssertionError("facade should not be called")
|
||||
|
||||
cells = builder.cells.prefixed("vendor_", facade=fail_if_called)
|
||||
cells["vendor_cell"] = Pattern()
|
||||
built, _report = builder.build(output="library")
|
||||
|
||||
assert set(built) == {"vendor_cell"}
|
||||
|
||||
|
||||
def test_build_cells_renamed_views_are_independent_and_accept_dynamic_names() -> None:
|
||||
builder = LibraryBuilder()
|
||||
upper = builder.cells.renamed(str.upper)
|
||||
suffixed = builder.cells.renamed(lambda name: name + "_v2")
|
||||
|
||||
builder.cells.plain = Pattern()
|
||||
upper["upper"] = Pattern()
|
||||
suffixed["other"] = Pattern()
|
||||
built, _report = builder.build(output="library")
|
||||
|
||||
assert set(built) == {"plain", "UPPER", "other_v2"}
|
||||
|
||||
|
||||
def test_build_cells_policy_registration_is_atomic() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder.cells.public = Pattern()
|
||||
prefixed = builder.cells.prefixed("impl_", facade=lambda pattern: pattern)
|
||||
|
||||
with pytest.raises(LibraryError, match="already exist"):
|
||||
prefixed.public = Pattern()
|
||||
|
||||
assert set(builder) == {"public"}
|
||||
assert "impl_public" not in builder
|
||||
|
||||
|
||||
def test_build_cells_policy_views_validate_names_and_transforms() -> None:
|
||||
builder = LibraryBuilder()
|
||||
invalid_name = builder.cells.renamed(lambda _name: None) # type: ignore[arg-type,return-value]
|
||||
|
||||
with pytest.raises(TypeError, match="expected str"):
|
||||
invalid_name["cell"] = Pattern()
|
||||
with pytest.raises(TypeError, match="must be callable"):
|
||||
builder.cells.prefixed("x_", facade=object()) # type: ignore[arg-type]
|
||||
|
||||
assert not builder
|
||||
|
||||
|
||||
def test_build_cells_view_deletion_requires_concrete_names_after_rename() -> None:
|
||||
builder = LibraryBuilder()
|
||||
prefixed = builder.cells.prefixed("impl_", facade=lambda pattern: pattern)
|
||||
prefixed["cell"] = Pattern()
|
||||
|
||||
with pytest.raises(BuildError, match="Delete concrete names"):
|
||||
del prefixed["cell"]
|
||||
|
||||
del builder.cells["cell"]
|
||||
del builder.cells["impl_cell"]
|
||||
builder.cells["plain"] = Pattern()
|
||||
del builder.cells["plain"]
|
||||
assert not builder
|
||||
|
||||
|
||||
def test_build_cells_item_reads_remain_write_only() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder.cells["cell"] = Pattern()
|
||||
|
||||
with pytest.raises(BuildError, match="write-only"):
|
||||
_ = builder.cells["cell"]
|
||||
|
||||
|
||||
def test_build_cells_prefixed_view_normalizes_recipe_tree_and_generates_facade() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder.cells["_helper"] = Pattern()
|
||||
cells = builder.cells.prefixed(
|
||||
"impl_",
|
||||
postprocess=lambda pattern: Pattern(ports={"out": Port((7, 8), 0)}, refs=pattern.refs),
|
||||
facade=lambda pattern: ports_to_data(pattern, (40, 0)),
|
||||
)
|
||||
|
||||
def make_tree() -> Library:
|
||||
tree = Library({"_helper": Pattern(), "old_top": Pattern()})
|
||||
tree["old_top"].ref("_helper")
|
||||
return tree
|
||||
|
||||
cells.tree = cell(make_tree)()
|
||||
built, report = builder.build(output="library")
|
||||
|
||||
assert {"_helper", "impl_tree", "tree"} <= set(built)
|
||||
helper_targets = set(built["impl_tree"].refs)
|
||||
assert "_helper" not in helper_targets
|
||||
assert len(helper_targets) == 1
|
||||
renamed_helper = helper_targets.pop()
|
||||
assert renamed_helper in built
|
||||
assert report.provenance[renamed_helper].kind == "helper"
|
||||
assert report.provenance[renamed_helper].requested_name == "_helper"
|
||||
assert report.provenance[renamed_helper].owner_declared_name == "impl_tree"
|
||||
assert set(built["tree"].ports) == {"out"}
|
||||
assert set(built["tree"].refs) == {"impl_tree"}
|
||||
|
||||
|
||||
def test_build_cells_view_accepts_direct_tree_and_rejects_multiple_tops() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder.cells["tree"] = Library({"old_top": Pattern()})
|
||||
built, _report = builder.build(output="library")
|
||||
assert set(built) == {"tree"}
|
||||
|
||||
invalid = LibraryBuilder()
|
||||
invalid.cells["tree"] = cell(lambda: Library({"one": Pattern(), "two": Pattern()}))()
|
||||
with pytest.raises(BuildError, match="exactly one topcell"):
|
||||
invalid.validate()
|
||||
|
||||
|
||||
def test_build_cells_tree_declared_name_wins_over_single_use_helper() -> None:
|
||||
builder = LibraryBuilder()
|
||||
|
||||
def make_tree() -> Library:
|
||||
top = Pattern()
|
||||
top.ref("_device")
|
||||
return Library({"_device": Pattern(), "old_top": top})
|
||||
|
||||
builder.cells["_device"] = cell(make_tree)()
|
||||
built, report = builder.build(output="library")
|
||||
|
||||
assert "_device" in built
|
||||
assert set(built["_device"].refs) != {"_device"}
|
||||
assert len(built["_device"].refs) == 1
|
||||
helper_name = next(iter(built["_device"].refs))
|
||||
assert helper_name in built
|
||||
assert report.provenance["_device"].kind == "declared"
|
||||
assert report.provenance["_device"].requested_name == "_device"
|
||||
assert report.provenance[helper_name].kind == "helper"
|
||||
assert report.provenance[helper_name].requested_name == "_device"
|
||||
assert report.provenance[helper_name].owner_declared_name == "_device"
|
||||
|
||||
|
||||
def test_build_cells_tree_declared_name_rejects_public_helper_conflict() -> None:
|
||||
builder = LibraryBuilder()
|
||||
|
||||
def make_tree() -> Library:
|
||||
top = Pattern()
|
||||
top.ref("device")
|
||||
return Library({"device": Pattern(), "old_top": top})
|
||||
|
||||
builder.cells["device"] = cell(make_tree)()
|
||||
with pytest.raises(BuildError, match="Unresolved duplicate key"):
|
||||
builder.validate()
|
||||
|
||||
del builder.cells["device"]
|
||||
builder.cells["device"] = Pattern()
|
||||
builder.validate()
|
||||
|
||||
|
||||
def test_build_cells_replacement_postprocess_updates_self_published_recipe_and_facade() -> None:
|
||||
builder = LibraryBuilder()
|
||||
cells = builder.cells.prefixed(
|
||||
"impl_",
|
||||
postprocess=lambda _pattern: Pattern(ports={"new": Port((5, 6), 0)}),
|
||||
facade=lambda pattern: ports_to_data(pattern, (50, 0)),
|
||||
)
|
||||
|
||||
def make_device(lib: ILibrary) -> Pattern:
|
||||
original = Pattern(ports={"old": Port((1, 2), 0)})
|
||||
lib["impl_device"] = original
|
||||
return original
|
||||
|
||||
cells.device = cell(make_device)(builder.library)
|
||||
built, report = builder.build(output="library")
|
||||
|
||||
assert set(built) == {"impl_device", "device"}
|
||||
assert set(built["impl_device"].ports) == {"new"}
|
||||
assert set(built["device"].ports) == {"new"}
|
||||
assert built["device"].labels[(50, 0)][0].string.startswith("new:")
|
||||
assert report.provenance["impl_device"].kind == "declared"
|
||||
assert report.dependency_graph["device"] == frozenset({"impl_device"})
|
||||
|
||||
|
||||
def test_build_cells_rejects_unrelated_self_published_pattern() -> None:
|
||||
builder = LibraryBuilder()
|
||||
|
||||
def make_device(lib: ILibrary) -> Pattern:
|
||||
lib["device"] = Pattern()
|
||||
return Pattern()
|
||||
|
||||
builder.cells.device = cell(make_device)(builder.library)
|
||||
with pytest.raises(BuildError, match="wrote a different pattern"):
|
||||
builder.validate()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue