[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
|
|
@ -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):
|
||||
raise TypeError('LibraryBuilder recipes must be wrapped with cell(fn)(...) or @cell.')
|
||||
declaration = value
|
||||
elif callable(value):
|
||||
raise TypeError('LibraryBuilder recipes must be wrapped with cell(fn)(...) or @cell.')
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue