[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()
|
builder = LibraryBuilder()
|
||||||
cells = builder.cells
|
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
|
# Load some devices from a GDS file
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass, replace
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
|
|
@ -16,7 +17,7 @@ from .mapping import Library, LibraryView
|
||||||
from .overlay import OverlayLibrary
|
from .overlay import OverlayLibrary
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable, Iterator, KeysView, Mapping, Sequence
|
from collections.abc import Callable, Iterator, KeysView, Sequence
|
||||||
|
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
|
|
||||||
|
|
@ -76,8 +77,8 @@ class BuildReport:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _BuildRecipe:
|
class _BuildRecipe:
|
||||||
""" Captured deferred call to a pattern factory. """
|
""" Captured deferred call to a pattern or tree factory. """
|
||||||
func: Callable[..., Pattern]
|
func: Callable[..., Pattern | TreeView]
|
||||||
args: tuple[Any, ...]
|
args: tuple[Any, ...]
|
||||||
kwargs: dict[str, Any]
|
kwargs: dict[str, Any]
|
||||||
explicit_dependencies: tuple[str, ...] = ()
|
explicit_dependencies: tuple[str, ...] = ()
|
||||||
|
|
@ -87,9 +88,16 @@ class _BuildRecipe:
|
||||||
return self
|
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`.
|
Use as either `cell(fn)(...)` or `@cell`.
|
||||||
"""
|
"""
|
||||||
|
|
@ -111,19 +119,138 @@ class _LibraryPlaceholder:
|
||||||
return '<LibraryBuilder.library>'
|
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:
|
class BuildCellsView:
|
||||||
"""
|
"""
|
||||||
Attribute-based declaration namespace for `LibraryBuilder`.
|
Attribute-based declaration namespace for `LibraryBuilder`.
|
||||||
|
|
||||||
This is the ergonomic authoring surface exposed as `builder.cells`. It is
|
This is the ergonomic authoring surface exposed as `builder.cells`. It is
|
||||||
intentionally write-focused: attribute assignment and deletion register
|
intentionally write-focused: attribute or item assignment and deletion
|
||||||
declarations, while attribute reads fail with guidance to build first and
|
register declarations, while reads fail with guidance to build first and
|
||||||
use the returned library.
|
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, '_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:
|
def __getattr__(self, name: str) -> Pattern:
|
||||||
raise BuildError(
|
raise BuildError(
|
||||||
|
|
@ -131,16 +258,47 @@ class BuildCellsView:
|
||||||
'Call build() and index the returned library instead.'
|
'Call build() and index the returned library instead.'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __setattr__(self, name: str, value: Pattern | _BuildRecipe) -> None:
|
def __getitem__(self, name: str) -> Pattern:
|
||||||
if name == '_library':
|
raise BuildError(
|
||||||
object.__setattr__(self, name, value)
|
f'LibraryBuilder.cells[{name!r}] is write-only during authoring. '
|
||||||
return
|
'Call build() and index the returned library instead.'
|
||||||
self._library[name] = value
|
)
|
||||||
|
|
||||||
|
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:
|
def __delattr__(self, name: str) -> None:
|
||||||
if name == '_library':
|
if name in self.__slots__:
|
||||||
raise AttributeError(name)
|
raise AttributeError(name)
|
||||||
del self._library[name]
|
del self[name]
|
||||||
|
|
||||||
|
|
||||||
class LibraryBuilder(INameView):
|
class LibraryBuilder(INameView):
|
||||||
|
|
@ -148,8 +306,8 @@ class LibraryBuilder(INameView):
|
||||||
Two-phase declaration surface for mixed imported/generated libraries.
|
Two-phase declaration surface for mixed imported/generated libraries.
|
||||||
|
|
||||||
A `LibraryBuilder` collects three kinds of inputs:
|
A `LibraryBuilder` collects three kinds of inputs:
|
||||||
- direct declared `Pattern` objects
|
- direct declared `Pattern` objects or single-top trees
|
||||||
- deferred recipes created with `cell(...)`
|
- deferred Pattern or tree recipes created with `cell(...)`
|
||||||
- imported source-backed library views added with `add_source(...)`
|
- imported source-backed library views added with `add_source(...)`
|
||||||
|
|
||||||
The builder itself is not a normal readable library during authoring.
|
The builder itself is not a normal readable library during authoring.
|
||||||
|
|
@ -164,7 +322,7 @@ class LibraryBuilder(INameView):
|
||||||
self._library_placeholder = _LibraryPlaceholder(self)
|
self._library_placeholder = _LibraryPlaceholder(self)
|
||||||
self._frozen = False
|
self._frozen = False
|
||||||
self._building = False
|
self._building = False
|
||||||
self._declarations: dict[str, Pattern | _BuildRecipe] = {}
|
self._declarations: dict[str, _BuildDeclaration] = {}
|
||||||
self._sources: list[tuple[ILibraryView, dict[str, str]]] = []
|
self._sources: list[tuple[ILibraryView, dict[str, str]]] = []
|
||||||
self._names: dict[str, None] = {}
|
self._names: dict[str, None] = {}
|
||||||
|
|
||||||
|
|
@ -194,11 +352,16 @@ class LibraryBuilder(INameView):
|
||||||
def __setitem__(
|
def __setitem__(
|
||||||
self,
|
self,
|
||||||
key: str,
|
key: str,
|
||||||
value: Pattern | _BuildRecipe,
|
value: Pattern | TreeView | _BuildRecipe,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._assert_editable()
|
self._register_declarations(((key, value, None),))
|
||||||
if key in self._names:
|
|
||||||
raise LibraryError(f'"{key}" already exists in the builder. Overwriting is not allowed!')
|
def _prepare_declaration(
|
||||||
|
self,
|
||||||
|
value: Pattern | TreeView | _BuildRecipe,
|
||||||
|
postprocess: Callable[[Pattern], Pattern] | None,
|
||||||
|
) -> _BuildDeclaration:
|
||||||
|
from ..pattern import Pattern # noqa: PLC0415
|
||||||
|
|
||||||
if isinstance(value, _BuildRecipe):
|
if isinstance(value, _BuildRecipe):
|
||||||
placeholders = (
|
placeholders = (
|
||||||
|
|
@ -207,14 +370,57 @@ class LibraryBuilder(INameView):
|
||||||
)
|
)
|
||||||
if any(placeholder is not self.library for placeholder in placeholders):
|
if any(placeholder is not self.library for placeholder in placeholders):
|
||||||
raise BuildError('A recipe cannot use another LibraryBuilder.library placeholder.')
|
raise BuildError('A recipe cannot use another LibraryBuilder.library placeholder.')
|
||||||
declaration = value
|
elif callable(value):
|
||||||
else:
|
raise TypeError('LibraryBuilder recipes must be wrapped with cell(fn)(...) or @cell.')
|
||||||
if callable(value):
|
elif not isinstance(value, Pattern | Mapping):
|
||||||
raise TypeError('LibraryBuilder recipes must be wrapped with cell(fn)(...) or @cell.')
|
raise TypeError(
|
||||||
declaration = value
|
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
|
def _register_declarations(
|
||||||
self._names[key] = None
|
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:
|
def __delitem__(self, key: str) -> None:
|
||||||
self._assert_editable()
|
self._assert_editable()
|
||||||
|
|
@ -431,6 +637,23 @@ class LibraryBuilder(INameView):
|
||||||
return session, report
|
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):
|
class _BuildSessionLibrary(ILibrary):
|
||||||
"""
|
"""
|
||||||
Internal overlay-backed library used while a `LibraryBuilder` is executing.
|
Internal overlay-backed library used while a `LibraryBuilder` is executing.
|
||||||
|
|
@ -629,6 +852,75 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
return
|
return
|
||||||
raise BuildError(f'Missing dependency "{name}"')
|
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:
|
def _ensure_declared(self, name: str) -> None:
|
||||||
from ..pattern import Pattern # noqa: PLC0415
|
from ..pattern import Pattern # noqa: PLC0415
|
||||||
|
|
||||||
|
|
@ -639,30 +931,44 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
raise BuildError(f'Cycle detected while building declared cells: {chain}')
|
raise BuildError(f'Cycle detected while building declared cells: {chain}')
|
||||||
|
|
||||||
declaration = self._builder._declarations[name]
|
declaration = self._builder._declarations[name]
|
||||||
|
value = declaration.value
|
||||||
self._declared_stack.append(name)
|
self._declared_stack.append(name)
|
||||||
try:
|
try:
|
||||||
if isinstance(declaration, _BuildRecipe):
|
if isinstance(value, _BuildRecipe):
|
||||||
for dep in declaration.explicit_dependencies:
|
for dep in value.explicit_dependencies:
|
||||||
self._ensure_named(dep)
|
self._ensure_named(dep)
|
||||||
args = tuple(
|
args = tuple(
|
||||||
self if arg is self._builder.library else arg
|
self if arg is self._builder.library else arg
|
||||||
for arg in declaration.args
|
for arg in value.args
|
||||||
)
|
)
|
||||||
kwargs = {
|
kwargs = {
|
||||||
key: self if value is self._builder.library else value
|
key: self if arg_value is self._builder.library else arg_value
|
||||||
for key, value in declaration.kwargs.items()
|
for key, arg_value in value.kwargs.items()
|
||||||
}
|
}
|
||||||
pattern = declaration.func(*args, **kwargs)
|
result = value.func(*args, **kwargs)
|
||||||
if not isinstance(pattern, Pattern):
|
|
||||||
raise BuildError(f'Recipe for "{name}" returned {type(pattern).__name__}, expected Pattern') # noqa: TRY301
|
|
||||||
else:
|
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 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
|
raise BuildError( # noqa: TRY301
|
||||||
f'Recipe for "{name}" wrote a different pattern into the session under its own name.'
|
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:
|
else:
|
||||||
self[name] = pattern
|
self[name] = pattern
|
||||||
self._built.add(name)
|
self._built.add(name)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ from ..library import (
|
||||||
)
|
)
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..ports import Port
|
from ..ports import Port
|
||||||
|
from ..utils.ports2data import ports_to_data
|
||||||
|
|
||||||
|
|
||||||
def _owned_by(report: BuildReport, owner: str) -> set[str]:
|
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 child_calls == 1
|
||||||
assert set(built) == {"top", "child", "leaf", "unrelated"}
|
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