[wip] introduce BuildLibrary and make overlay first-class
This commit is contained in:
parent
e108199bcd
commit
6d494142fe
7 changed files with 1335 additions and 98 deletions
|
|
@ -20,10 +20,10 @@ Contents
|
||||||
* Use `Pather` to snap ports together into a circuit
|
* Use `Pather` to snap ports together into a circuit
|
||||||
* Check for dangling references
|
* Check for dangling references
|
||||||
- [library](library.py)
|
- [library](library.py)
|
||||||
* Continue from `devices.py` using a lazy library
|
* Continue from `devices.py` by declaring a mixed library with `BuildLibrary`
|
||||||
* Create a `LazyLibrary`, which loads / generates patterns only when they are first used
|
* Import source-backed GDS cells and register python-generated recipes together
|
||||||
|
* Call `build()` to produce a normal library for downstream `Pather` usage and writing
|
||||||
* Explore alternate ways of specifying a pattern for `.plug()` and `.place()`
|
* Explore alternate ways of specifying a pattern for `.plug()` and `.place()`
|
||||||
* Design a pattern which is meant to plug into an existing pattern (via `.interface()`)
|
|
||||||
- [pather](pather.py)
|
- [pather](pather.py)
|
||||||
* Use `Pather` to route individual wires and wire bundles
|
* Use `Pather` to route individual wires and wire bundles
|
||||||
* Use `AutoTool` to generate paths
|
* Use `AutoTool` to generate paths
|
||||||
|
|
|
||||||
|
|
@ -1,136 +1,114 @@
|
||||||
"""
|
"""
|
||||||
Tutorial: using a source-backed lazy GDS library and `Pather.interface()`.
|
Tutorial: authoring a mixed library with `BuildLibrary`.
|
||||||
|
|
||||||
This example assumes you have already read `devices.py` and generated the
|
This example assumes you have already read `devices.py` and generated the
|
||||||
`circuit.gds` file it writes. The goal here is not the photonic-crystal geometry
|
`circuit.gds` file it writes. The goal here is not the photonic-crystal geometry
|
||||||
itself, but rather how Masque lets you mix lazily loaded GDS content with
|
itself, but rather how Masque lets you combine imported GDS cells with
|
||||||
python-generated devices inside one library.
|
python-generated recipes, then turn that declaration set into a normal library
|
||||||
|
for downstream assembly and writing.
|
||||||
"""
|
"""
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
|
|
||||||
from masque import Pather
|
from masque import BuildLibrary, Pather, Pattern, cell
|
||||||
from masque.file.gdsii import writefile
|
from masque.file.gdsii import writefile
|
||||||
from masque.file.gdsii_lazy import OverlayLibrary, readfile
|
from masque.file.gdsii_lazy import readfile
|
||||||
|
|
||||||
import basic_shapes
|
import basic_shapes
|
||||||
import devices
|
import devices
|
||||||
from basic_shapes import GDS_OPTS
|
from basic_shapes import GDS_OPTS
|
||||||
|
|
||||||
|
|
||||||
|
def make_mixed_waveguide(lib: BuildLibrary) -> Pattern:
|
||||||
|
"""
|
||||||
|
Recipe which assembles imported and generated cells behind the builder API.
|
||||||
|
"""
|
||||||
|
circ = Pather(library=lib, ports='tri_l3cav')
|
||||||
|
|
||||||
|
# First way to specify what we are plugging in: request an explicit abstract.
|
||||||
|
circ.plug(lib.abstract('wg10'), {'input': 'right'})
|
||||||
|
|
||||||
|
# Second way: use an AbstractView, which behaves like a mapping of names
|
||||||
|
# to abstracts.
|
||||||
|
abstracts = lib.abstract_view()
|
||||||
|
circ.plug(abstracts['wg10'], {'output': 'left'})
|
||||||
|
|
||||||
|
# Third way: let Pather resolve a pattern name through its own library.
|
||||||
|
circ.plug('tri_wg10', {'input': 'right'})
|
||||||
|
circ.plug('tri_wg10', {'output': 'left'})
|
||||||
|
|
||||||
|
return circ.pattern
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
# `OverlayLibrary` lets us mix source-backed GDS cells with python-generated
|
builder = BuildLibrary()
|
||||||
# patterns behind the same library interface.
|
cells = builder.cells
|
||||||
lib = OverlayLibrary()
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Load some devices from a GDS file
|
# Load some devices from a GDS file
|
||||||
#
|
#
|
||||||
|
|
||||||
# Scan circuit.gds and prepare to lazy-load its contents. Port labels are
|
# Scan circuit.gds and prepare to lazy-load its contents. Port labels are
|
||||||
# imported on first materialization, but the raw source remains untouched.
|
# imported on first materialization, but the raw source remains untouched
|
||||||
|
# until we build the final library.
|
||||||
gds_lib, _properties = readfile('circuit.gds')
|
gds_lib, _properties = readfile('circuit.gds')
|
||||||
lib.add_source(gds_lib.with_ports_from_data(layers=[(3, 0)], max_depth=1))
|
builder.add_source(gds_lib.with_ports_from_data(layers=[(3, 0)], max_depth=1))
|
||||||
|
|
||||||
print('Patterns loaded from GDS into library:\n' + pformat(list(lib.keys())))
|
print('Registered imported cells:\n' + pformat(list(gds_lib.keys())))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Add some new devices to the library, this time from python code rather than GDS
|
# Register some new devices, this time from python code rather than GDS.
|
||||||
#
|
#
|
||||||
|
|
||||||
lib['triangle'] = basic_shapes.triangle(devices.RADIUS)
|
cells.triangle = basic_shapes.triangle(devices.RADIUS)
|
||||||
opts: dict[str, Any] = dict(
|
opts: dict[str, Any] = dict(
|
||||||
lattice_constant=devices.LATTICE_CONSTANT,
|
lattice_constant=devices.LATTICE_CONSTANT,
|
||||||
hole='triangle',
|
hole='triangle',
|
||||||
)
|
)
|
||||||
|
|
||||||
lib['tri_wg10'] = devices.waveguide(length=10, mirror_periods=5, **opts)
|
cells.tri_wg10 = cell(devices.waveguide)(length=10, mirror_periods=5, **opts)
|
||||||
lib['tri_wg05'] = devices.waveguide(length=5, mirror_periods=5, **opts)
|
cells.tri_wg05 = cell(devices.waveguide)(length=5, mirror_periods=5, **opts)
|
||||||
lib['tri_wg28'] = devices.waveguide(length=28, mirror_periods=5, **opts)
|
cells.tri_wg28 = cell(devices.waveguide)(length=28, mirror_periods=5, **opts)
|
||||||
lib['tri_bend0'] = devices.bend(mirror_periods=5, **opts)
|
cells.tri_bend0 = cell(devices.bend)(mirror_periods=5, **opts)
|
||||||
lib['tri_ysplit'] = devices.y_splitter(mirror_periods=5, **opts)
|
cells.tri_ysplit = cell(devices.y_splitter)(mirror_periods=5, **opts)
|
||||||
lib['tri_l3cav'] = devices.perturbed_l3(xy_size=(4, 10), **opts, hole_lib=lib)
|
cells.tri_l3cav = cell(devices.perturbed_l3)(xy_size=(4, 10), **opts, hole_lib=builder)
|
||||||
|
cells.mixed_wg_cav = cell(make_mixed_waveguide)(builder)
|
||||||
|
|
||||||
|
print('Declared cells waiting to be built:\n' + pformat(list(builder.keys())))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Build a mixed waveguide with an L3 cavity in the middle
|
# Build the declaration set into a normal library.
|
||||||
#
|
#
|
||||||
|
|
||||||
# Start a new design by copying the ports from an existing library cell.
|
built = builder.build()
|
||||||
# This gives `circ2` the same external interface as `tri_l3cav`.
|
print('Built library contains:\n' + pformat(list(built.keys())))
|
||||||
circ2 = Pather(library=lib, ports='tri_l3cav')
|
|
||||||
|
|
||||||
# First way to specify what we are plugging in: request an explicit abstract.
|
|
||||||
# This works with `Pattern` methods directly as well as with `Pather`.
|
|
||||||
circ2.plug(lib.abstract('wg10'), {'input': 'right'})
|
|
||||||
|
|
||||||
# Second way: use an `AbstractView`, which behaves like a mapping of names
|
|
||||||
# to abstracts.
|
|
||||||
abstracts = lib.abstract_view()
|
|
||||||
circ2.plug(abstracts['wg10'], {'output': 'left'})
|
|
||||||
|
|
||||||
# Third way: let `Pather` resolve a pattern name through its own library.
|
|
||||||
# This shorthand is convenient, but it is specific to helpers that already
|
|
||||||
# carry a library reference.
|
|
||||||
circ2.plug('tri_wg10', {'input': 'right'})
|
|
||||||
circ2.plug('tri_wg10', {'output': 'left'})
|
|
||||||
|
|
||||||
# Add the circuit to the device library.
|
|
||||||
lib['mixed_wg_cav'] = circ2.pattern
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Build a second device that is explicitly designed to mate with `circ2`.
|
# Continue designing against the built library.
|
||||||
#
|
#
|
||||||
|
|
||||||
# `Pather.interface()` makes a new pattern whose ports mirror an existing
|
# The built result behaves like a normal mutable library, so downstream code
|
||||||
# design's external interface. That is useful when you want to design an
|
# can use Pather, abstract views, and writing without going back through the
|
||||||
# adapter, continuation, or mating structure.
|
# builder interface.
|
||||||
circ3 = Pather.interface(source=circ2)
|
circ = Pather.interface(source='mixed_wg_cav', library=built)
|
||||||
|
circ.plug('tri_bend0', {'input': 'right'})
|
||||||
# Continue routing outward from those inherited ports.
|
circ.plug('tri_bend0', {'input': 'left'}, mirrored=True) # mirror since no tri y-symmetry
|
||||||
circ3.plug('tri_bend0', {'input': 'right'})
|
circ.plug('tri_bend0', {'input': 'right'})
|
||||||
circ3.plug('tri_bend0', {'input': 'left'}, mirrored=True) # mirror since no tri y-symmetry
|
circ.plug('bend0', {'output': 'left'})
|
||||||
circ3.plug('tri_bend0', {'input': 'right'})
|
circ.plug('bend0', {'output': 'left'})
|
||||||
circ3.plug('bend0', {'output': 'left'})
|
circ.plug('bend0', {'output': 'left'})
|
||||||
circ3.plug('bend0', {'output': 'left'})
|
circ.plug('tri_wg10', {'input': 'right'})
|
||||||
circ3.plug('bend0', {'output': 'left'})
|
circ.plug('tri_wg28', {'input': 'right'})
|
||||||
circ3.plug('tri_wg10', {'input': 'right'})
|
circ.plug('tri_wg10', {'input': 'right', 'output': 'left'})
|
||||||
circ3.plug('tri_wg28', {'input': 'right'})
|
built['loop_segment'] = circ.pattern
|
||||||
circ3.plug('tri_wg10', {'input': 'right', 'output': 'left'})
|
|
||||||
|
|
||||||
lib['loop_segment'] = circ3.pattern
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Write all devices into a GDS file
|
# Write all devices into a GDS file.
|
||||||
#
|
#
|
||||||
print('Writing library to file...')
|
print('Writing library to file...')
|
||||||
writefile(lib, 'library.gds', **GDS_OPTS)
|
writefile(built, 'library.gds', **GDS_OPTS)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
#class prout:
|
|
||||||
# def place(
|
|
||||||
# self,
|
|
||||||
# other: Pattern,
|
|
||||||
# label_layer: layer_t = 'WATLAYER',
|
|
||||||
# *,
|
|
||||||
# port_map: Dict[str, str | None] | None = None,
|
|
||||||
# **kwargs,
|
|
||||||
# ) -> 'prout':
|
|
||||||
#
|
|
||||||
# Pattern.place(self, other, port_map=port_map, **kwargs)
|
|
||||||
# name: str | None
|
|
||||||
# for name in other.ports:
|
|
||||||
# if port_map:
|
|
||||||
# assert(name is not None)
|
|
||||||
# name = port_map.get(name, name)
|
|
||||||
# if name is None:
|
|
||||||
# continue
|
|
||||||
# self.pattern.label(string=name, offset=self.ports[name].offset, layer=label_layer)
|
|
||||||
# return self
|
|
||||||
#
|
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,15 @@ from .library import (
|
||||||
ILibrary as ILibrary,
|
ILibrary as ILibrary,
|
||||||
LibraryView as LibraryView,
|
LibraryView as LibraryView,
|
||||||
Library as Library,
|
Library as Library,
|
||||||
|
BuiltLibrary as BuiltLibrary,
|
||||||
|
BuildLibrary as BuildLibrary,
|
||||||
|
BuildReport as BuildReport,
|
||||||
|
CellProvenance as CellProvenance,
|
||||||
LazyLibrary as LazyLibrary,
|
LazyLibrary as LazyLibrary,
|
||||||
AbstractView as AbstractView,
|
AbstractView as AbstractView,
|
||||||
TreeView as TreeView,
|
TreeView as TreeView,
|
||||||
Tree as Tree,
|
Tree as Tree,
|
||||||
|
cell as cell,
|
||||||
)
|
)
|
||||||
from .ports import (
|
from .ports import (
|
||||||
Port as Port,
|
Port as Port,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
"""
|
"""
|
||||||
Source-backed lazy GDSII reader using the pure-python klamath path.
|
Classic source-backed lazy GDSII reader built on the pure-python klamath path.
|
||||||
|
|
||||||
This module mirrors the lazy Arrow reader's interface closely enough to share
|
This module provides the non-Arrow half of Masque's lazy GDS architecture:
|
||||||
the same overlay and ports-import helpers, while still materializing cells
|
|
||||||
through the classic `gdsii` decoder.
|
- `GdsLibrarySource` scans a GDS stream once to discover library metadata,
|
||||||
|
struct order, and child edges without materializing every cell.
|
||||||
|
- cells are materialized on demand through the classic `gdsii` decoder
|
||||||
|
whenever a caller indexes the lazy view
|
||||||
|
- the source can be wrapped in `PortsLibraryView` or merged through
|
||||||
|
`OverlayLibrary`, both of which live in `gdsii_lazy_core`
|
||||||
|
|
||||||
|
The public surface intentionally parallels `gdsii_lazy_arrow` closely so that
|
||||||
|
callers can swap between the classic and Arrow-backed implementations with
|
||||||
|
minimal changes.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -36,6 +45,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _SourceHandle:
|
class _SourceHandle:
|
||||||
|
""" Owns the underlying stream and any companion file handle for a source. """
|
||||||
path: pathlib.Path | None
|
path: pathlib.Path | None
|
||||||
stream: IO[bytes]
|
stream: IO[bytes]
|
||||||
handle: IO[bytes] | None = None
|
handle: IO[bytes] | None = None
|
||||||
|
|
@ -49,6 +59,7 @@ class _SourceHandle:
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class _CellScan:
|
class _CellScan:
|
||||||
|
""" Scan-time metadata for one cell in the source stream. """
|
||||||
offset: int
|
offset: int
|
||||||
children: set[str]
|
children: set[str]
|
||||||
|
|
||||||
|
|
@ -107,6 +118,10 @@ class GdsLibrarySource(ILibraryView):
|
||||||
|
|
||||||
Cells are scanned once up front to discover order and child edges, then
|
Cells are scanned once up front to discover order and child edges, then
|
||||||
materialized one at a time through the classic `gdsii.read_elements` path.
|
materialized one at a time through the classic `gdsii.read_elements` path.
|
||||||
|
|
||||||
|
The source owns the stream lifetime, preserves on-disk ordering through
|
||||||
|
`source_order()`, and answers graph queries from scan metadata whenever
|
||||||
|
possible so callers can inspect hierarchy without forcing a full load.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,18 @@
|
||||||
"""
|
"""
|
||||||
Shared helpers for source-backed lazy GDS views.
|
Shared helpers for source-backed lazy GDS views.
|
||||||
|
|
||||||
|
This module contains the reusable pieces that sit between lazy source readers
|
||||||
|
and ordinary mutable library usage:
|
||||||
|
|
||||||
|
- `PortsLibraryView` layers a processed, ports-importing cache on top of a raw
|
||||||
|
source view without mutating the source itself
|
||||||
|
- `OverlayLibrary` exposes a mutable library surface that can mix source-backed
|
||||||
|
cells with overlay-owned materialized patterns
|
||||||
|
- the write helpers preserve source-backed copy-through behavior where
|
||||||
|
possible, falling back to normal pattern serialization when a cell has been
|
||||||
|
materialized or remapped
|
||||||
|
|
||||||
|
Both the classic and Arrow-backed lazy GDS readers rely on these helpers.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -30,6 +43,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _SourceLayer:
|
class _SourceLayer:
|
||||||
|
""" One imported source layer tracked by an `OverlayLibrary`. """
|
||||||
library: ILibraryView
|
library: ILibraryView
|
||||||
source_to_visible: dict[str, str]
|
source_to_visible: dict[str, str]
|
||||||
visible_to_source: dict[str, str]
|
visible_to_source: dict[str, str]
|
||||||
|
|
@ -39,6 +53,7 @@ class _SourceLayer:
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class _SourceEntry:
|
class _SourceEntry:
|
||||||
|
""" Reference to a single visible source-backed cell in an overlay. """
|
||||||
layer_index: int
|
layer_index: int
|
||||||
source_name: str
|
source_name: str
|
||||||
|
|
||||||
|
|
@ -73,6 +88,10 @@ class PortsLibraryView(ILibraryView):
|
||||||
|
|
||||||
The wrapped source remains untouched; this view owns a separate processed
|
The wrapped source remains untouched; this view owns a separate processed
|
||||||
cache so direct-copy workflows can continue to use the raw source view.
|
cache so direct-copy workflows can continue to use the raw source view.
|
||||||
|
|
||||||
|
Graph queries, source ordering, and copy-through capabilities are delegated
|
||||||
|
to the wrapped source whenever possible, while `__getitem__` and
|
||||||
|
`materialize_many()` return port-imported patterns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -231,6 +250,14 @@ class OverlayLibrary(ILibrary):
|
||||||
Source-backed cells remain lazy until accessed through `__getitem__`, at
|
Source-backed cells remain lazy until accessed through `__getitem__`, at
|
||||||
which point that visible cell is promoted into an overlay-owned materialized
|
which point that visible cell is promoted into an overlay-owned materialized
|
||||||
`Pattern`.
|
`Pattern`.
|
||||||
|
|
||||||
|
This is the main mutable integration surface for lazy GDS content. It lets
|
||||||
|
callers:
|
||||||
|
- expose one or more source-backed libraries behind a normal `ILibrary`
|
||||||
|
interface
|
||||||
|
- add or replace cells with overlay-owned patterns
|
||||||
|
- rename visible source cells
|
||||||
|
- remap references without immediately rewriting untouched source structs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
|
@ -522,6 +549,20 @@ class OverlayLibrary(ILibrary):
|
||||||
return tuple(name for name in self._order if name in self._entries)
|
return tuple(name for name in self._order if name in self._entries)
|
||||||
|
|
||||||
|
|
||||||
|
class BuiltOverlayLibrary(OverlayLibrary):
|
||||||
|
"""
|
||||||
|
Internal overlay output returned by `BuildLibrary.build(output='overlay')`.
|
||||||
|
|
||||||
|
The type is intentionally not part of the public API. It exists so build
|
||||||
|
outputs can carry a `build_report` while still behaving like an
|
||||||
|
`OverlayLibrary`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *, build_report: Any | None = None) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.build_report = build_report
|
||||||
|
|
||||||
|
|
||||||
def _iter_library_infos(library: Mapping[str, Pattern] | ILibraryView) -> Iterator[dict[str, Any]]:
|
def _iter_library_infos(library: Mapping[str, Pattern] | ILibraryView) -> Iterator[dict[str, Any]]:
|
||||||
info = getattr(library, 'library_info', None)
|
info = getattr(library, 'library_info', None)
|
||||||
if isinstance(info, dict):
|
if isinstance(info, dict):
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ Classes include:
|
||||||
- `AbstractView`: Provides a way to use []-indexing to generate abstracts for patterns in the linked
|
- `AbstractView`: Provides a way to use []-indexing to generate abstracts for patterns in the linked
|
||||||
library. Generated with `ILibraryView.abstract_view()`.
|
library. Generated with `ILibraryView.abstract_view()`.
|
||||||
"""
|
"""
|
||||||
from typing import Self, TYPE_CHECKING, cast, TypeAlias, Protocol, Literal
|
from typing import Self, TYPE_CHECKING, Any, cast, TypeAlias, Protocol, Literal
|
||||||
from collections.abc import Iterator, Mapping, MutableMapping, Sequence, Callable
|
from collections.abc import Iterator, Mapping, MutableMapping, Sequence, Callable
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
@ -22,12 +22,14 @@ import copy
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
from contextvars import ContextVar
|
||||||
|
from dataclasses import dataclass, replace
|
||||||
from graphlib import TopologicalSorter, CycleError
|
from graphlib import TopologicalSorter, CycleError
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import ArrayLike, NDArray
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
|
||||||
from .error import LibraryError, PatternError
|
from .error import BuildError, LibraryError, PatternError
|
||||||
from .utils import layer_t, apply_transforms
|
from .utils import layer_t, apply_transforms
|
||||||
from .shapes import Shape, Polygon
|
from .shapes import Shape, Polygon
|
||||||
from .label import Label
|
from .label import Label
|
||||||
|
|
@ -40,6 +42,11 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_ACTIVE_BUILD_SESSIONS: ContextVar[dict[int, '_BuildSessionLibrary'] | None] = ContextVar(
|
||||||
|
'masque_active_build_sessions',
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class visitor_function_t(Protocol):
|
class visitor_function_t(Protocol):
|
||||||
""" Signature for `Library.dfs()` visitor functions. """
|
""" Signature for `Library.dfs()` visitor functions. """
|
||||||
|
|
@ -62,6 +69,69 @@ Tree: TypeAlias = MutableMapping[str, 'Pattern']
|
||||||
dangling_mode_t: TypeAlias = Literal['error', 'ignore', 'include']
|
dangling_mode_t: TypeAlias = Literal['error', 'ignore', 'include']
|
||||||
""" How helpers should handle refs whose targets are not present in the library. """
|
""" How helpers should handle refs whose targets are not present in the library. """
|
||||||
|
|
||||||
|
emitted_via_t: TypeAlias = Literal['declaration', 'helper_write', 'tree_merge', 'source_import']
|
||||||
|
""" Build-provenance origin tags for emitted cells. """
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CellProvenance:
|
||||||
|
"""
|
||||||
|
Provenance record for one cell in a completed build output.
|
||||||
|
|
||||||
|
Each output name in a `BuildReport` maps to one `CellProvenance`. The
|
||||||
|
record captures both where the cell came from and how its visible name was
|
||||||
|
chosen.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
final_name: Name exposed by the completed library.
|
||||||
|
requested_name: First name requested for this cell during the build.
|
||||||
|
kind: Whether the cell came from a declaration, helper emission, or an
|
||||||
|
imported source library.
|
||||||
|
owner_declared_name: Declared cell responsible for this output cell, if
|
||||||
|
any. Imported source cells leave this as `None`.
|
||||||
|
emitted_via: High-level path by which the cell entered the output.
|
||||||
|
build_chain: Declared-cell dependency chain that was active when the
|
||||||
|
cell was emitted.
|
||||||
|
renamed_from: Original requested name when the final name differs.
|
||||||
|
source_name: Original on-source name for imported cells.
|
||||||
|
source_metadata: Optional source-library metadata copied through from
|
||||||
|
lazy GDS readers.
|
||||||
|
"""
|
||||||
|
final_name: str
|
||||||
|
requested_name: str
|
||||||
|
kind: Literal['declared', 'helper', 'source']
|
||||||
|
owner_declared_name: str | None
|
||||||
|
emitted_via: emitted_via_t
|
||||||
|
build_chain: tuple[str, ...]
|
||||||
|
renamed_from: str | None = None
|
||||||
|
source_name: str | None = None
|
||||||
|
source_metadata: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BuildReport:
|
||||||
|
"""
|
||||||
|
Immutable summary of one `BuildLibrary.validate()` or `.build()` run.
|
||||||
|
|
||||||
|
The report is designed to answer two questions after a build completes:
|
||||||
|
which declared cells depended on which other declared cells, and where each
|
||||||
|
output cell came from.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
requested_roots: Roots explicitly requested for the run. A full
|
||||||
|
`build()` uses all declared cells.
|
||||||
|
provenance: Mapping from final output name to provenance metadata.
|
||||||
|
owned_cells: Mapping from declared cell name to all final output cell
|
||||||
|
names it owns, including helper cells emitted while that declared
|
||||||
|
cell was building.
|
||||||
|
dependency_graph: Declared-cell dependency graph discovered through
|
||||||
|
library-mediated reads and explicit recipe hints.
|
||||||
|
"""
|
||||||
|
requested_roots: tuple[str, ...]
|
||||||
|
provenance: Mapping[str, CellProvenance]
|
||||||
|
owned_cells: Mapping[str, tuple[str, ...]]
|
||||||
|
dependency_graph: Mapping[str, frozenset[str]]
|
||||||
|
|
||||||
|
|
||||||
SINGLE_USE_PREFIX = '_'
|
SINGLE_USE_PREFIX = '_'
|
||||||
"""
|
"""
|
||||||
|
|
@ -1397,6 +1467,819 @@ class Library(ILibrary):
|
||||||
return tree, pat
|
return tree, pat
|
||||||
|
|
||||||
|
|
||||||
|
class BuiltLibrary(Library):
|
||||||
|
"""
|
||||||
|
Eager library returned by `BuildLibrary.build(output='library')`.
|
||||||
|
|
||||||
|
This is a normal materialized `Library` with one additional attribute,
|
||||||
|
`build_report`, which records how the library was assembled from
|
||||||
|
declarations, helper emissions, and imported source-backed cells.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mapping: MutableMapping[str, 'Pattern'] | None = None,
|
||||||
|
*,
|
||||||
|
build_report: BuildReport | None = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(mapping=mapping)
|
||||||
|
self.build_report = build_report
|
||||||
|
|
||||||
|
|
||||||
|
class _CellFactory:
|
||||||
|
"""
|
||||||
|
Adapter that turns a plain pattern factory into a deferred recipe factory.
|
||||||
|
|
||||||
|
Calling the wrapper captures arguments and returns a `_BuildRecipe`
|
||||||
|
instead of executing the function immediately.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, func: Callable[..., 'Pattern']) -> None:
|
||||||
|
self.func = func
|
||||||
|
self.__name__ = getattr(func, '__name__', type(self).__name__)
|
||||||
|
self.__doc__ = getattr(func, '__doc__')
|
||||||
|
|
||||||
|
def __call__(self, *args: Any, **kwargs: Any) -> '_BuildRecipe':
|
||||||
|
return _BuildRecipe(func=self.func, args=args, kwargs=kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _BuildRecipe:
|
||||||
|
""" Captured deferred call to a pattern factory. """
|
||||||
|
func: Callable[..., 'Pattern']
|
||||||
|
args: tuple[Any, ...]
|
||||||
|
kwargs: dict[str, Any]
|
||||||
|
explicit_dependencies: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
def depends_on(self, *names: str) -> '_BuildRecipe':
|
||||||
|
self.explicit_dependencies += tuple(names)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class _PatternDeclaration:
|
||||||
|
""" Declared cell backed by an already-built `Pattern`. """
|
||||||
|
pattern: 'Pattern'
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class _RecipeDeclaration:
|
||||||
|
""" Declared cell backed by a deferred recipe. """
|
||||||
|
recipe: _BuildRecipe
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class _SourceDeclaration:
|
||||||
|
"""
|
||||||
|
Imported source-backed names registered with a `BuildLibrary`.
|
||||||
|
|
||||||
|
The declaration stores visible-name remapping plus pre-scanned graph
|
||||||
|
metadata. Underlying source cells stay lazy until a build session
|
||||||
|
materializes or copies them through.
|
||||||
|
"""
|
||||||
|
library: ILibraryView
|
||||||
|
source_to_visible: Mapping[str, str]
|
||||||
|
visible_to_source: Mapping[str, str]
|
||||||
|
child_graph: Mapping[str, set[str]]
|
||||||
|
order: tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
|
def cell(func: Callable[..., 'Pattern']) -> _CellFactory:
|
||||||
|
"""
|
||||||
|
Wrap a plain pattern factory so calls return deferred build recipes.
|
||||||
|
|
||||||
|
Use as either `cell(fn)(...)` or `@cell`.
|
||||||
|
"""
|
||||||
|
return _CellFactory(func)
|
||||||
|
|
||||||
|
|
||||||
|
class BuildCellsView:
|
||||||
|
"""
|
||||||
|
Attribute-based declaration namespace for `BuildLibrary`.
|
||||||
|
|
||||||
|
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
|
||||||
|
use the returned library.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, library: 'BuildLibrary') -> None:
|
||||||
|
object.__setattr__(self, '_library', library)
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> 'Pattern':
|
||||||
|
raise BuildError(
|
||||||
|
f'BuildLibrary.cells.{name} is write-only during authoring. '
|
||||||
|
'Call build() and index the returned library instead.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __setattr__(self, name: str, value: 'Pattern | _BuildRecipe') -> None:
|
||||||
|
if name.startswith('_'):
|
||||||
|
object.__setattr__(self, name, value)
|
||||||
|
return
|
||||||
|
self._library[name] = value
|
||||||
|
|
||||||
|
def __delattr__(self, name: str) -> None:
|
||||||
|
if name.startswith('_'):
|
||||||
|
raise AttributeError(name)
|
||||||
|
del self._library[name]
|
||||||
|
|
||||||
|
|
||||||
|
class BuildLibrary(ILibrary):
|
||||||
|
"""
|
||||||
|
Two-phase declaration surface for mixed imported/generated libraries.
|
||||||
|
|
||||||
|
A `BuildLibrary` collects three kinds of inputs:
|
||||||
|
- direct declared `Pattern` objects
|
||||||
|
- deferred recipes created with `cell(...)`
|
||||||
|
- imported source-backed library views added with `add_source(...)`
|
||||||
|
|
||||||
|
The builder itself is not a normal readable library during authoring.
|
||||||
|
Instead, `validate()` and `build()` create a temporary build-session library
|
||||||
|
that recipes can read from and write helper cells into while dependencies
|
||||||
|
are resolved. `build()` then freezes the builder on success and returns a
|
||||||
|
normal library-like object carrying a `build_report`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *, check_on_register: bool = False) -> None:
|
||||||
|
self.check_on_register = check_on_register
|
||||||
|
self.cells = BuildCellsView(self)
|
||||||
|
self.last_build_report: BuildReport | None = None
|
||||||
|
self._frozen = False
|
||||||
|
self._declarations: dict[str, _PatternDeclaration | _RecipeDeclaration] = {}
|
||||||
|
self._sources: list[_SourceDeclaration] = []
|
||||||
|
self._names: set[str] = set()
|
||||||
|
self._order: list[str] = []
|
||||||
|
|
||||||
|
def _active_session(self) -> '_BuildSessionLibrary | None':
|
||||||
|
sessions = _ACTIVE_BUILD_SESSIONS.get()
|
||||||
|
if sessions is None:
|
||||||
|
return None
|
||||||
|
return sessions.get(id(self))
|
||||||
|
|
||||||
|
def _require_active_session(self, operation: str) -> '_BuildSessionLibrary':
|
||||||
|
session = self._active_session()
|
||||||
|
if session is None:
|
||||||
|
raise BuildError(
|
||||||
|
f'BuildLibrary.{operation}() is only available while validate() or build() is running. '
|
||||||
|
'Use the built output library for reads.'
|
||||||
|
)
|
||||||
|
return session
|
||||||
|
|
||||||
|
def _assert_editable(self) -> None:
|
||||||
|
if self._frozen:
|
||||||
|
raise BuildError('This BuildLibrary has already been built successfully and is now frozen.')
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
session = self._active_session()
|
||||||
|
if session is not None:
|
||||||
|
return iter(session)
|
||||||
|
return iter(self._order)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
session = self._active_session()
|
||||||
|
if session is not None:
|
||||||
|
return len(session)
|
||||||
|
return len(self._names)
|
||||||
|
|
||||||
|
def __contains__(self, key: object) -> bool:
|
||||||
|
session = self._active_session()
|
||||||
|
if session is not None:
|
||||||
|
return key in session
|
||||||
|
return key in self._names
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> 'Pattern':
|
||||||
|
return self._require_active_session('__getitem__')[key]
|
||||||
|
|
||||||
|
def __setitem__(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
value: 'Pattern | _BuildRecipe | Callable[[], Pattern]',
|
||||||
|
) -> None:
|
||||||
|
session = self._active_session()
|
||||||
|
if session is not None:
|
||||||
|
session[key] = value
|
||||||
|
return
|
||||||
|
|
||||||
|
self._assert_editable()
|
||||||
|
if key in self._names:
|
||||||
|
raise LibraryError(f'"{key}" already exists in the builder. Overwriting is not allowed!')
|
||||||
|
|
||||||
|
declaration: _PatternDeclaration | _RecipeDeclaration
|
||||||
|
if isinstance(value, _BuildRecipe):
|
||||||
|
declaration = _RecipeDeclaration(value)
|
||||||
|
else:
|
||||||
|
if callable(value):
|
||||||
|
raise TypeError('BuildLibrary recipes must be wrapped with cell(fn)(...) or @cell.')
|
||||||
|
declaration = _PatternDeclaration(value)
|
||||||
|
|
||||||
|
self._declarations[key] = declaration
|
||||||
|
self._names.add(key)
|
||||||
|
self._order.append(key)
|
||||||
|
|
||||||
|
if self.check_on_register:
|
||||||
|
try:
|
||||||
|
self.validate(names=(key,))
|
||||||
|
except Exception:
|
||||||
|
del self._declarations[key]
|
||||||
|
self._names.remove(key)
|
||||||
|
self._order.remove(key)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def __delitem__(self, key: str) -> None:
|
||||||
|
session = self._active_session()
|
||||||
|
if session is not None:
|
||||||
|
del session[key]
|
||||||
|
return
|
||||||
|
|
||||||
|
self._assert_editable()
|
||||||
|
if key not in self._declarations:
|
||||||
|
raise KeyError(key)
|
||||||
|
del self._declarations[key]
|
||||||
|
self._names.remove(key)
|
||||||
|
self._order.remove(key)
|
||||||
|
|
||||||
|
def _merge(self, key_self: str, other: Mapping[str, 'Pattern'], key_other: str) -> None:
|
||||||
|
session = self._active_session()
|
||||||
|
if session is not None:
|
||||||
|
session._merge(key_self, other, key_other)
|
||||||
|
return
|
||||||
|
self[key_self] = copy.deepcopy(other[key_other])
|
||||||
|
|
||||||
|
def add(
|
||||||
|
self,
|
||||||
|
other: Mapping[str, 'Pattern'],
|
||||||
|
rename_theirs: Callable[['ILibraryView', str], str] = _rename_patterns,
|
||||||
|
mutate_other: bool = False,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
session = self._active_session()
|
||||||
|
if session is not None:
|
||||||
|
return session.add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
|
||||||
|
return super().add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
|
||||||
|
|
||||||
|
def rename(
|
||||||
|
self,
|
||||||
|
old_name: str,
|
||||||
|
new_name: str,
|
||||||
|
move_references: bool = False,
|
||||||
|
) -> Self:
|
||||||
|
"""
|
||||||
|
Rename an imported source-backed visible name during authoring.
|
||||||
|
|
||||||
|
Only imported source-backed cells may be renamed on the builder itself.
|
||||||
|
Declared/generated cells must be registered under their intended final
|
||||||
|
names. `move_references=True` is intentionally unsupported here because
|
||||||
|
deferred recipes and declaration internals cannot be rewritten safely.
|
||||||
|
"""
|
||||||
|
session = self._active_session()
|
||||||
|
if session is not None:
|
||||||
|
session.rename(old_name, new_name, move_references=move_references)
|
||||||
|
return self
|
||||||
|
|
||||||
|
self._assert_editable()
|
||||||
|
if old_name == new_name:
|
||||||
|
return self
|
||||||
|
if old_name in self._declarations:
|
||||||
|
raise BuildError(
|
||||||
|
f'Cannot rename declared build cell "{old_name}" during authoring. '
|
||||||
|
'Register it under the intended final name instead.'
|
||||||
|
)
|
||||||
|
if old_name not in self._names:
|
||||||
|
raise LibraryError(f'"{old_name}" does not exist in the builder.')
|
||||||
|
if new_name in self._names:
|
||||||
|
raise LibraryError(f'"{new_name}" already exists in the builder.')
|
||||||
|
if move_references:
|
||||||
|
raise BuildError(
|
||||||
|
'BuildLibrary.rename(..., move_references=True) is not supported for imported source cells. '
|
||||||
|
'Builder-level renames only change the visible imported name.'
|
||||||
|
)
|
||||||
|
|
||||||
|
source_index = next(
|
||||||
|
(idx for idx, spec in enumerate(self._sources) if old_name in spec.visible_to_source),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if source_index is None:
|
||||||
|
raise BuildError(
|
||||||
|
f'Cannot rename "{old_name}" during authoring because only imported source-backed '
|
||||||
|
'cells may be renamed on a BuildLibrary.'
|
||||||
|
)
|
||||||
|
|
||||||
|
spec = self._sources[source_index]
|
||||||
|
source_name = spec.visible_to_source[old_name]
|
||||||
|
source_to_visible = dict(spec.source_to_visible)
|
||||||
|
visible_to_source = dict(spec.visible_to_source)
|
||||||
|
order = list(spec.order)
|
||||||
|
|
||||||
|
source_to_visible[source_name] = new_name
|
||||||
|
del visible_to_source[old_name]
|
||||||
|
visible_to_source[new_name] = source_name
|
||||||
|
order[order.index(old_name)] = new_name
|
||||||
|
|
||||||
|
self._sources[source_index] = replace(
|
||||||
|
spec,
|
||||||
|
source_to_visible=source_to_visible,
|
||||||
|
visible_to_source=visible_to_source,
|
||||||
|
order=tuple(order),
|
||||||
|
)
|
||||||
|
self._names.remove(old_name)
|
||||||
|
self._names.add(new_name)
|
||||||
|
self._order[self._order.index(old_name)] = new_name
|
||||||
|
return self
|
||||||
|
|
||||||
|
def abstract(self, name: str) -> Abstract:
|
||||||
|
return self._require_active_session('abstract').abstract(name)
|
||||||
|
|
||||||
|
def resolve(
|
||||||
|
self,
|
||||||
|
other: 'Abstract | str | Pattern | TreeView',
|
||||||
|
append: bool = False,
|
||||||
|
) -> 'Abstract | Pattern':
|
||||||
|
return self._require_active_session('resolve').resolve(other, append=append)
|
||||||
|
|
||||||
|
def add_source(
|
||||||
|
self,
|
||||||
|
source: Mapping[str, 'Pattern'] | ILibraryView,
|
||||||
|
*,
|
||||||
|
rename_theirs: Callable[[ILibraryView, str], str] | None = None,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Register an imported source-backed library with the builder.
|
||||||
|
|
||||||
|
The source is not materialized immediately. Instead, its names and
|
||||||
|
child graph are scanned once and stored as an import declaration. The
|
||||||
|
source may be renamed on entry to avoid collisions with existing
|
||||||
|
declarations or other imported sources.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Mapping of `{source_name: visible_name}` for imported names that
|
||||||
|
were renamed while being added.
|
||||||
|
"""
|
||||||
|
self._assert_editable()
|
||||||
|
|
||||||
|
view = source if isinstance(source, ILibraryView) else LibraryView(source)
|
||||||
|
source_order = tuple(view.source_order())
|
||||||
|
child_graph = view.child_graph(dangling='include')
|
||||||
|
|
||||||
|
source_to_visible: dict[str, str] = {}
|
||||||
|
visible_to_source: dict[str, str] = {}
|
||||||
|
rename_map: dict[str, str] = {}
|
||||||
|
new_names: list[str] = []
|
||||||
|
|
||||||
|
for name in source_order:
|
||||||
|
visible = name
|
||||||
|
if visible in self._names or visible in visible_to_source:
|
||||||
|
if rename_theirs is None:
|
||||||
|
raise LibraryError(f'Conflicting name while adding source: {name!r}')
|
||||||
|
visible = rename_theirs(self, name)
|
||||||
|
if visible in self._names or visible in visible_to_source:
|
||||||
|
raise LibraryError(f'Unresolved duplicate key encountered while adding source: {name!r} -> {visible!r}')
|
||||||
|
rename_map[name] = visible
|
||||||
|
source_to_visible[name] = visible
|
||||||
|
visible_to_source[visible] = name
|
||||||
|
new_names.append(visible)
|
||||||
|
|
||||||
|
self._sources.append(_SourceDeclaration(
|
||||||
|
library=view,
|
||||||
|
source_to_visible=dict(source_to_visible),
|
||||||
|
visible_to_source=dict(visible_to_source),
|
||||||
|
child_graph={name: set(children) for name, children in child_graph.items()},
|
||||||
|
order=tuple(source_to_visible[name] for name in source_order),
|
||||||
|
))
|
||||||
|
for visible in new_names:
|
||||||
|
self._names.add(visible)
|
||||||
|
self._order.append(visible)
|
||||||
|
return rename_map
|
||||||
|
|
||||||
|
def validate(
|
||||||
|
self,
|
||||||
|
names: Sequence[str] | None = None,
|
||||||
|
*,
|
||||||
|
allow_dangling: bool = False,
|
||||||
|
) -> BuildReport:
|
||||||
|
"""
|
||||||
|
Run the full build logic and return a `BuildReport` without producing output.
|
||||||
|
|
||||||
|
This is a dry run over the same dependency resolution and recipe
|
||||||
|
execution path used by `build()`. Any generated library is discarded
|
||||||
|
after validation completes.
|
||||||
|
"""
|
||||||
|
report, _output = self._run_build(names=names, output='overlay', allow_dangling=allow_dangling, persist_output=False)
|
||||||
|
self.last_build_report = report
|
||||||
|
return report
|
||||||
|
|
||||||
|
def build(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
output: Literal['overlay', 'library'] = 'overlay',
|
||||||
|
allow_dangling: bool = False,
|
||||||
|
) -> 'BuiltLibrary | ILibrary':
|
||||||
|
"""
|
||||||
|
Materialize declarations and return a usable output library.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output: `'overlay'` preserves imported source-backed cells where
|
||||||
|
possible, while `'library'` eagerly materializes the full
|
||||||
|
result.
|
||||||
|
allow_dangling: If `False`, fail the build when the completed
|
||||||
|
library still contains dangling references.
|
||||||
|
"""
|
||||||
|
self._assert_editable()
|
||||||
|
report, built_output = self._run_build(names=None, output=output, allow_dangling=allow_dangling, persist_output=True)
|
||||||
|
self._frozen = True
|
||||||
|
self.last_build_report = report
|
||||||
|
return built_output
|
||||||
|
|
||||||
|
def _run_build(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
names: Sequence[str] | None,
|
||||||
|
output: Literal['overlay', 'library'],
|
||||||
|
allow_dangling: bool,
|
||||||
|
persist_output: bool,
|
||||||
|
) -> tuple[BuildReport, BuiltLibrary | ILibrary | None]:
|
||||||
|
roots = tuple(dict.fromkeys(names if names is not None else self._declarations.keys()))
|
||||||
|
unknown = [name for name in roots if name not in self._names]
|
||||||
|
if unknown:
|
||||||
|
raise BuildError(f'Unknown build roots requested: {unknown}')
|
||||||
|
|
||||||
|
session = _BuildSessionLibrary(self)
|
||||||
|
sessions = dict(_ACTIVE_BUILD_SESSIONS.get() or {})
|
||||||
|
sessions[id(self)] = session
|
||||||
|
token = _ACTIVE_BUILD_SESSIONS.set(sessions)
|
||||||
|
try:
|
||||||
|
session.materialize_many(roots)
|
||||||
|
if not allow_dangling:
|
||||||
|
session.child_graph(dangling='error')
|
||||||
|
if output == 'library':
|
||||||
|
built_output = session.to_library() if persist_output else None
|
||||||
|
elif persist_output:
|
||||||
|
built_output = session.to_overlay()
|
||||||
|
else:
|
||||||
|
built_output = None
|
||||||
|
finally:
|
||||||
|
_ACTIVE_BUILD_SESSIONS.reset(token)
|
||||||
|
|
||||||
|
report = session.build_report(roots)
|
||||||
|
if built_output is not None:
|
||||||
|
built_output.build_report = report
|
||||||
|
return report, built_output
|
||||||
|
|
||||||
|
|
||||||
|
class _BuildSessionLibrary(ILibrary):
|
||||||
|
"""
|
||||||
|
Internal overlay-backed library used while a `BuildLibrary` is executing.
|
||||||
|
|
||||||
|
This object provides the mutable-library surface that recipes expect while
|
||||||
|
also tracking declared-cell dependencies, helper-cell provenance, and
|
||||||
|
imported source cells. It exists only for the duration of a validation or
|
||||||
|
build run.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, builder: BuildLibrary) -> None:
|
||||||
|
from .file.gdsii_lazy_core import BuiltOverlayLibrary, _SourceEntry, _SourceLayer # noqa: PLC0415
|
||||||
|
|
||||||
|
self._builder = builder
|
||||||
|
self._overlay = BuiltOverlayLibrary()
|
||||||
|
self._source_entry_type = _SourceEntry
|
||||||
|
self._source_layer_type = _SourceLayer
|
||||||
|
self._states: dict[str, Literal['unbuilt', 'building', 'built']] = {
|
||||||
|
name: 'unbuilt' for name in builder._declarations
|
||||||
|
}
|
||||||
|
self._declared_stack: list[str] = []
|
||||||
|
self._emission_stack: list[str] = []
|
||||||
|
self._emission_via_stack: list[emitted_via_t] = []
|
||||||
|
self._names = set(builder._names)
|
||||||
|
self._order = list(builder._order)
|
||||||
|
self._provenance: dict[str, CellProvenance] = {}
|
||||||
|
self._owned_cells: defaultdict[str, list[str]] = defaultdict(list)
|
||||||
|
self._dependency_graph: defaultdict[str, set[str]] = defaultdict(set)
|
||||||
|
self._install_sources()
|
||||||
|
|
||||||
|
def _install_sources(self) -> None:
|
||||||
|
for spec in self._builder._sources:
|
||||||
|
layer = self._source_layer_type(
|
||||||
|
library=spec.library,
|
||||||
|
source_to_visible=dict(spec.source_to_visible),
|
||||||
|
visible_to_source=dict(spec.visible_to_source),
|
||||||
|
child_graph={name: set(children) for name, children in spec.child_graph.items()},
|
||||||
|
order=list(spec.order),
|
||||||
|
)
|
||||||
|
layer_index = len(self._overlay._layers)
|
||||||
|
self._overlay._layers.append(layer)
|
||||||
|
source_info = getattr(spec.library, 'library_info', None)
|
||||||
|
source_meta = dict(source_info) if isinstance(source_info, dict) else None
|
||||||
|
|
||||||
|
for source_name, visible_name in spec.source_to_visible.items():
|
||||||
|
self._overlay._entries[visible_name] = self._source_entry_type(
|
||||||
|
layer_index=layer_index,
|
||||||
|
source_name=source_name,
|
||||||
|
)
|
||||||
|
if visible_name not in self._overlay._order:
|
||||||
|
self._overlay._order.append(visible_name)
|
||||||
|
self._provenance[visible_name] = CellProvenance(
|
||||||
|
final_name=visible_name,
|
||||||
|
requested_name=source_name,
|
||||||
|
kind='source',
|
||||||
|
owner_declared_name=None,
|
||||||
|
emitted_via='source_import',
|
||||||
|
build_chain=(),
|
||||||
|
renamed_from=source_name if visible_name != source_name else None,
|
||||||
|
source_name=source_name,
|
||||||
|
source_metadata=source_meta,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
return (name for name in self._order if name in self._names)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._names)
|
||||||
|
|
||||||
|
def __contains__(self, key: object) -> bool:
|
||||||
|
return key in self._names or key in self._overlay
|
||||||
|
|
||||||
|
def _touch_name(self, key: str) -> None:
|
||||||
|
if key not in self._names:
|
||||||
|
self._names.add(key)
|
||||||
|
self._order.append(key)
|
||||||
|
|
||||||
|
def _current_declared(self) -> str | None:
|
||||||
|
if not self._declared_stack:
|
||||||
|
return None
|
||||||
|
return self._declared_stack[-1]
|
||||||
|
|
||||||
|
def _record_dependency(self, target: str) -> None:
|
||||||
|
current = self._current_declared()
|
||||||
|
if current is None or current == target or target not in self._builder._declarations:
|
||||||
|
return
|
||||||
|
self._dependency_graph[current].add(target)
|
||||||
|
|
||||||
|
def _guard_mutable_output_name(self, key: str, *, operation: str) -> None:
|
||||||
|
if key in self._builder._declarations:
|
||||||
|
raise BuildError(f'Cannot {operation} declared build cell "{key}" during an active build session.')
|
||||||
|
|
||||||
|
provenance = self._provenance.get(key)
|
||||||
|
if provenance is not None and provenance.kind == 'source':
|
||||||
|
raise BuildError(f'Cannot {operation} imported source cell "{key}" during an active build session.')
|
||||||
|
|
||||||
|
def _remove_owned_cell(self, owner: str | None, name: str) -> None:
|
||||||
|
if owner is None or owner not in self._owned_cells:
|
||||||
|
return
|
||||||
|
cells = self._owned_cells[owner]
|
||||||
|
self._owned_cells[owner] = [cell for cell in cells if cell != name]
|
||||||
|
if not self._owned_cells[owner]:
|
||||||
|
del self._owned_cells[owner]
|
||||||
|
|
||||||
|
def rename(
|
||||||
|
self,
|
||||||
|
old_name: str,
|
||||||
|
new_name: str,
|
||||||
|
move_references: bool = False,
|
||||||
|
) -> Self:
|
||||||
|
if old_name == new_name:
|
||||||
|
return self
|
||||||
|
if old_name not in self._overlay:
|
||||||
|
if old_name in self._builder._declarations:
|
||||||
|
self._guard_mutable_output_name(old_name, operation='rename')
|
||||||
|
raise LibraryError(f'"{old_name}" does not exist in the library.')
|
||||||
|
|
||||||
|
self._guard_mutable_output_name(old_name, operation='rename')
|
||||||
|
if new_name in self._names:
|
||||||
|
raise LibraryError(f'"{new_name}" already exists in the library.')
|
||||||
|
|
||||||
|
self._overlay.rename(old_name, new_name, move_references=move_references)
|
||||||
|
self._names.discard(old_name)
|
||||||
|
self._names.add(new_name)
|
||||||
|
if old_name in self._order:
|
||||||
|
idx = self._order.index(old_name)
|
||||||
|
self._order[idx] = new_name
|
||||||
|
|
||||||
|
provenance = self._provenance.pop(old_name)
|
||||||
|
requested_name = provenance.requested_name
|
||||||
|
self._provenance[new_name] = replace(
|
||||||
|
provenance,
|
||||||
|
final_name=new_name,
|
||||||
|
renamed_from=requested_name if new_name != requested_name else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
owner = provenance.owner_declared_name
|
||||||
|
if owner is not None and owner in self._owned_cells:
|
||||||
|
self._owned_cells[owner] = [
|
||||||
|
new_name if cell_name == old_name else cell_name
|
||||||
|
for cell_name in self._owned_cells[owner]
|
||||||
|
]
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> 'Pattern':
|
||||||
|
if key in self._builder._declarations:
|
||||||
|
self._record_dependency(key)
|
||||||
|
self._ensure_declared(key)
|
||||||
|
return self._overlay[key]
|
||||||
|
|
||||||
|
def __setitem__(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
value: 'Pattern | Callable[[], Pattern]',
|
||||||
|
) -> None:
|
||||||
|
if key in self._overlay:
|
||||||
|
raise LibraryError(f'"{key}" already exists in the library. Overwriting is not allowed!')
|
||||||
|
current = self._current_declared()
|
||||||
|
if key in self._builder._declarations and key != current:
|
||||||
|
raise LibraryError(f'"{key}" is reserved for a declared cell and cannot be used as a helper name.')
|
||||||
|
|
||||||
|
pattern = value() if callable(value) else value
|
||||||
|
self._overlay[key] = pattern
|
||||||
|
self._touch_name(key)
|
||||||
|
|
||||||
|
kind: Literal['declared', 'helper']
|
||||||
|
via = self._emission_via_stack[-1] if self._emission_via_stack else 'helper_write'
|
||||||
|
if current is not None and key == current:
|
||||||
|
kind = 'declared'
|
||||||
|
via = 'declaration'
|
||||||
|
else:
|
||||||
|
kind = 'helper'
|
||||||
|
if not self._emission_via_stack:
|
||||||
|
via = 'helper_write'
|
||||||
|
|
||||||
|
self._emission_stack.append(key)
|
||||||
|
try:
|
||||||
|
self._record_provenance(
|
||||||
|
final_name=key,
|
||||||
|
requested_name=key,
|
||||||
|
kind=kind,
|
||||||
|
owner_declared_name=current if kind == 'helper' else key,
|
||||||
|
emitted_via=via,
|
||||||
|
build_chain=tuple(self._declared_stack),
|
||||||
|
renamed_from=None,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self._emission_stack.pop()
|
||||||
|
|
||||||
|
def __delitem__(self, key: str) -> None:
|
||||||
|
if key not in self._overlay:
|
||||||
|
if key in self._builder._declarations:
|
||||||
|
self._guard_mutable_output_name(key, operation='delete')
|
||||||
|
raise KeyError(key)
|
||||||
|
|
||||||
|
self._guard_mutable_output_name(key, operation='delete')
|
||||||
|
provenance = self._provenance.get(key)
|
||||||
|
if key in self._overlay:
|
||||||
|
del self._overlay[key]
|
||||||
|
self._names.discard(key)
|
||||||
|
if key in self._order:
|
||||||
|
self._order.remove(key)
|
||||||
|
self._provenance.pop(key, None)
|
||||||
|
if provenance is not None:
|
||||||
|
self._remove_owned_cell(provenance.owner_declared_name, key)
|
||||||
|
|
||||||
|
def _merge(self, key_self: str, other: Mapping[str, 'Pattern'], key_other: str) -> None:
|
||||||
|
self[key_self] = copy.deepcopy(other[key_other])
|
||||||
|
|
||||||
|
def add(
|
||||||
|
self,
|
||||||
|
other: Mapping[str, 'Pattern'],
|
||||||
|
rename_theirs: Callable[['ILibraryView', str], str] = _rename_patterns,
|
||||||
|
mutate_other: bool = False,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
self._emission_via_stack.append('tree_merge')
|
||||||
|
try:
|
||||||
|
rename_map = super().add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
|
||||||
|
finally:
|
||||||
|
self._emission_via_stack.pop()
|
||||||
|
|
||||||
|
current = self._current_declared()
|
||||||
|
for old_name, new_name in rename_map.items():
|
||||||
|
if new_name in self._provenance:
|
||||||
|
self._provenance[new_name] = replace(
|
||||||
|
self._provenance[new_name],
|
||||||
|
requested_name=old_name,
|
||||||
|
renamed_from=old_name,
|
||||||
|
owner_declared_name=current if current is not None else self._provenance[new_name].owner_declared_name,
|
||||||
|
)
|
||||||
|
return rename_map
|
||||||
|
|
||||||
|
def _record_provenance(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
final_name: str,
|
||||||
|
requested_name: str,
|
||||||
|
kind: Literal['declared', 'helper'],
|
||||||
|
owner_declared_name: str | None,
|
||||||
|
emitted_via: emitted_via_t,
|
||||||
|
build_chain: tuple[str, ...],
|
||||||
|
renamed_from: str | None,
|
||||||
|
) -> None:
|
||||||
|
self._provenance[final_name] = CellProvenance(
|
||||||
|
final_name=final_name,
|
||||||
|
requested_name=requested_name,
|
||||||
|
kind=kind,
|
||||||
|
owner_declared_name=owner_declared_name,
|
||||||
|
emitted_via=emitted_via,
|
||||||
|
build_chain=build_chain,
|
||||||
|
renamed_from=renamed_from,
|
||||||
|
)
|
||||||
|
if owner_declared_name is not None and final_name not in self._owned_cells[owner_declared_name]:
|
||||||
|
self._owned_cells[owner_declared_name].append(final_name)
|
||||||
|
|
||||||
|
def _wrap_error(self, name: str, exc: Exception) -> BuildError:
|
||||||
|
helper = self._emission_stack[-1] if self._emission_stack else None
|
||||||
|
chain = tuple(self._declared_stack)
|
||||||
|
msg = [f'Failed while building declared cell "{name}"']
|
||||||
|
if helper is not None and helper != name:
|
||||||
|
msg.append(f'while materializing helper/output "{helper}"')
|
||||||
|
if chain:
|
||||||
|
msg.append(f'Dependency chain: {" -> ".join(chain)}')
|
||||||
|
msg.append(f'Cause: {exc}')
|
||||||
|
return BuildError('\n'.join(msg))
|
||||||
|
|
||||||
|
def _ensure_named(self, name: str) -> None:
|
||||||
|
if name in self._builder._declarations:
|
||||||
|
self._record_dependency(name)
|
||||||
|
self._ensure_declared(name)
|
||||||
|
return
|
||||||
|
if name in self._overlay:
|
||||||
|
return
|
||||||
|
raise BuildError(f'Missing dependency "{name}"')
|
||||||
|
|
||||||
|
def _ensure_declared(self, name: str) -> None:
|
||||||
|
from .pattern import Pattern # noqa: PLC0415
|
||||||
|
|
||||||
|
state = self._states[name]
|
||||||
|
if state == 'built':
|
||||||
|
return
|
||||||
|
if state == 'building':
|
||||||
|
chain = ' -> '.join(self._declared_stack + [name])
|
||||||
|
raise BuildError(f'Cycle detected while building declared cells: {chain}')
|
||||||
|
|
||||||
|
declaration = self._builder._declarations[name]
|
||||||
|
self._states[name] = 'building'
|
||||||
|
self._declared_stack.append(name)
|
||||||
|
try:
|
||||||
|
if isinstance(declaration, _PatternDeclaration):
|
||||||
|
pattern = declaration.pattern.deepcopy()
|
||||||
|
else:
|
||||||
|
for dep in declaration.recipe.explicit_dependencies:
|
||||||
|
self._ensure_named(dep)
|
||||||
|
pattern = declaration.recipe.func(*declaration.recipe.args, **declaration.recipe.kwargs)
|
||||||
|
if not isinstance(pattern, Pattern):
|
||||||
|
raise BuildError(f'Recipe for "{name}" returned {type(pattern).__name__}, expected Pattern')
|
||||||
|
|
||||||
|
if name in self._overlay:
|
||||||
|
if self._overlay[name] is not pattern:
|
||||||
|
raise BuildError(
|
||||||
|
f'Recipe for "{name}" wrote a different pattern into the session under its own name.'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self[name] = pattern
|
||||||
|
self._states[name] = 'built'
|
||||||
|
except Exception as exc:
|
||||||
|
self._states[name] = 'unbuilt'
|
||||||
|
raise self._wrap_error(name, exc) from exc
|
||||||
|
finally:
|
||||||
|
self._declared_stack.pop()
|
||||||
|
|
||||||
|
def materialize_many(self, names: Sequence[str]) -> None:
|
||||||
|
for name in dict.fromkeys(names):
|
||||||
|
self._ensure_named(name)
|
||||||
|
|
||||||
|
def source_order(self) -> tuple[str, ...]:
|
||||||
|
return self._overlay.source_order()
|
||||||
|
|
||||||
|
def child_graph(
|
||||||
|
self,
|
||||||
|
dangling: dangling_mode_t = 'error',
|
||||||
|
) -> dict[str, set[str]]:
|
||||||
|
return self._overlay.child_graph(dangling=dangling)
|
||||||
|
|
||||||
|
def parent_graph(
|
||||||
|
self,
|
||||||
|
dangling: dangling_mode_t = 'error',
|
||||||
|
) -> dict[str, set[str]]:
|
||||||
|
return self._overlay.parent_graph(dangling=dangling)
|
||||||
|
|
||||||
|
def build_report(self, requested_roots: Sequence[str]) -> BuildReport:
|
||||||
|
dependency_graph = {
|
||||||
|
name: frozenset(self._dependency_graph.get(name, set()))
|
||||||
|
for name in self._builder._declarations
|
||||||
|
if name in self._dependency_graph or name in requested_roots
|
||||||
|
}
|
||||||
|
owned_cells = {
|
||||||
|
name: tuple(cells)
|
||||||
|
for name, cells in self._owned_cells.items()
|
||||||
|
}
|
||||||
|
return BuildReport(
|
||||||
|
requested_roots=tuple(dict.fromkeys(requested_roots)),
|
||||||
|
provenance=dict(self._provenance),
|
||||||
|
owned_cells=owned_cells,
|
||||||
|
dependency_graph=dependency_graph,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_overlay(self) -> ILibrary:
|
||||||
|
return self._overlay
|
||||||
|
|
||||||
|
def to_library(self) -> BuiltLibrary:
|
||||||
|
mapping = {name: self._overlay[name] for name in self._overlay.source_order()}
|
||||||
|
return BuiltLibrary(mapping)
|
||||||
|
|
||||||
|
|
||||||
class LazyLibrary(ILibrary):
|
class LazyLibrary(ILibrary):
|
||||||
"""
|
"""
|
||||||
This class is usually used to create a library of Patterns by mapping names to
|
This class is usually used to create a library of Patterns by mapping names to
|
||||||
|
|
|
||||||
315
masque/test/test_build_library.py
Normal file
315
masque/test/test_build_library.py
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ..builder import Pather
|
||||||
|
from ..error import BuildError
|
||||||
|
from ..library import BuildLibrary, BuiltLibrary, Library, cell
|
||||||
|
from ..pattern import Pattern
|
||||||
|
from ..ports import Port
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_traces_declared_dependencies_out_of_order() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
|
||||||
|
def make_parent(lib: BuildLibrary) -> Pattern:
|
||||||
|
pat = Pattern()
|
||||||
|
pat.ref("child")
|
||||||
|
assert lib.abstract("child").name == "child"
|
||||||
|
return pat
|
||||||
|
|
||||||
|
builder.cells.parent = cell(make_parent)(builder)
|
||||||
|
builder["child"] = Pattern(ports={"p": Port((0, 0), 0)})
|
||||||
|
|
||||||
|
built = builder.build()
|
||||||
|
|
||||||
|
assert "parent" in built
|
||||||
|
assert "child" in built
|
||||||
|
assert built.build_report.dependency_graph["parent"] == frozenset({"child"})
|
||||||
|
assert built.build_report.provenance["parent"].kind == "declared"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
|
||||||
|
def make_top(lib: BuildLibrary) -> Pattern:
|
||||||
|
tree = Library({"_helper": Pattern()})
|
||||||
|
name_a = lib << tree
|
||||||
|
name_b = lib << tree
|
||||||
|
top = Pattern()
|
||||||
|
top.ref(name_a)
|
||||||
|
top.ref(name_b)
|
||||||
|
return top
|
||||||
|
|
||||||
|
builder.cells.top = cell(make_top)(builder)
|
||||||
|
built = builder.build()
|
||||||
|
report = built.build_report
|
||||||
|
|
||||||
|
helpers = [
|
||||||
|
prov for prov in report.provenance.values()
|
||||||
|
if prov.owner_declared_name == "top" and prov.kind == "helper"
|
||||||
|
]
|
||||||
|
|
||||||
|
assert "top" in report.owned_cells["top"]
|
||||||
|
assert len(helpers) == 2
|
||||||
|
assert all(prov.emitted_via == "tree_merge" for prov in helpers)
|
||||||
|
assert any(prov.renamed_from == "_helper" for prov in helpers)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_requires_build_session_for_reads_and_freezes_after_build() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
builder["leaf"] = Pattern()
|
||||||
|
|
||||||
|
with pytest.raises(BuildError, match="validate\\(\\) or build\\(\\)"):
|
||||||
|
_ = builder["leaf"]
|
||||||
|
|
||||||
|
with pytest.raises(BuildError, match="write-only"):
|
||||||
|
_ = builder.cells.leaf
|
||||||
|
|
||||||
|
built = builder.build(output="library")
|
||||||
|
|
||||||
|
assert isinstance(built, BuiltLibrary)
|
||||||
|
assert built.build_report.requested_roots == ("leaf",)
|
||||||
|
|
||||||
|
with pytest.raises(BuildError, match="frozen"):
|
||||||
|
builder["later"] = Pattern()
|
||||||
|
with pytest.raises(BuildError, match="frozen"):
|
||||||
|
builder.build()
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_validate_is_retryable_after_failure() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
|
||||||
|
def make_parent(lib: BuildLibrary) -> Pattern:
|
||||||
|
pat = Pattern()
|
||||||
|
pat.ref("child")
|
||||||
|
lib.abstract("child")
|
||||||
|
return pat
|
||||||
|
|
||||||
|
builder.cells.parent = cell(make_parent)(builder)
|
||||||
|
|
||||||
|
with pytest.raises(BuildError, match='Failed while building declared cell "parent"'):
|
||||||
|
builder.validate()
|
||||||
|
|
||||||
|
builder["child"] = Pattern(ports={"p": Port((0, 0), 0)})
|
||||||
|
report = builder.validate()
|
||||||
|
|
||||||
|
assert report.dependency_graph["parent"] == frozenset({"child"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_check_on_register_rolls_back_failed_declarations() -> None:
|
||||||
|
builder = BuildLibrary(check_on_register=True)
|
||||||
|
|
||||||
|
def make_parent(lib: BuildLibrary) -> Pattern:
|
||||||
|
pat = Pattern()
|
||||||
|
pat.ref("child")
|
||||||
|
lib.abstract("child")
|
||||||
|
return pat
|
||||||
|
|
||||||
|
with pytest.raises(BuildError, match='Failed while building declared cell "parent"'):
|
||||||
|
builder.cells.parent = cell(make_parent)(builder)
|
||||||
|
|
||||||
|
assert "parent" not in builder
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_depends_on_supports_hidden_dependencies_for_partial_validation() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
builder["child"] = Pattern()
|
||||||
|
|
||||||
|
def make_parent() -> Pattern:
|
||||||
|
pat = Pattern()
|
||||||
|
pat.ref("child")
|
||||||
|
return pat
|
||||||
|
|
||||||
|
builder.cells.parent = cell(make_parent)().depends_on("child")
|
||||||
|
report = builder.validate(names=("parent",))
|
||||||
|
|
||||||
|
assert report.requested_roots == ("parent",)
|
||||||
|
assert report.dependency_graph["parent"] == frozenset({"child"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_validate_rejects_removed_output_argument() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
builder["leaf"] = Pattern()
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
builder.validate(output="library") # type: ignore[call-arg]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_allows_helper_writes_via_pather() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
builder["leaf"] = Pattern(ports={"a": Port((0, 0), 0)})
|
||||||
|
|
||||||
|
def make_top(lib: BuildLibrary) -> Pattern:
|
||||||
|
helper = Pather(library=lib, ports="leaf", name="_route")
|
||||||
|
top = Pattern()
|
||||||
|
top.ref("_route")
|
||||||
|
top.ref("leaf")
|
||||||
|
top.ports.update(helper.pattern.ports)
|
||||||
|
return top
|
||||||
|
|
||||||
|
builder.cells.top = cell(make_top)(builder)
|
||||||
|
built = builder.build()
|
||||||
|
|
||||||
|
helper_prov = built.build_report.provenance["_route"]
|
||||||
|
assert helper_prov.kind == "helper"
|
||||||
|
assert helper_prov.owner_declared_name == "top"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_preserves_source_cells_and_records_source_provenance() -> None:
|
||||||
|
source = Library({"src": Pattern()})
|
||||||
|
builder = BuildLibrary()
|
||||||
|
builder.add_source(source)
|
||||||
|
builder.cells.top = cell(lambda: Pattern())()
|
||||||
|
|
||||||
|
built = builder.build()
|
||||||
|
|
||||||
|
assert "src" in built
|
||||||
|
assert built.build_report.provenance["src"].kind == "source"
|
||||||
|
assert built.build_report.provenance["src"].emitted_via == "source_import"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_can_rename_imported_source_cells_during_authoring() -> None:
|
||||||
|
source = Library()
|
||||||
|
source["child"] = Pattern()
|
||||||
|
parent = Pattern()
|
||||||
|
parent.ref("child")
|
||||||
|
source["parent"] = parent
|
||||||
|
|
||||||
|
builder = BuildLibrary()
|
||||||
|
builder.add_source(source)
|
||||||
|
builder.rename("child", "renamed_child")
|
||||||
|
|
||||||
|
built = builder.build()
|
||||||
|
|
||||||
|
assert "renamed_child" in built
|
||||||
|
assert "child" not in built
|
||||||
|
assert "renamed_child" in built["parent"].refs
|
||||||
|
assert built.build_report.provenance["renamed_child"].source_name == "child"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_rejects_move_references_for_source_rename() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
builder.add_source(Library({"src": Pattern()}))
|
||||||
|
|
||||||
|
with pytest.raises(BuildError, match="move_references=True"):
|
||||||
|
builder.rename("src", "renamed_src", move_references=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_rejects_renaming_declared_cells_during_authoring() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
builder["declared"] = Pattern()
|
||||||
|
|
||||||
|
with pytest.raises(BuildError, match='Cannot rename declared build cell "declared"'):
|
||||||
|
builder.rename("declared", "renamed_declared")
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_helper_rename_updates_provenance_and_owned_cells() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
|
||||||
|
def make_top(lib: BuildLibrary) -> Pattern:
|
||||||
|
lib["_helper"] = Pattern()
|
||||||
|
lib.rename("_helper", "final_helper")
|
||||||
|
top = Pattern()
|
||||||
|
top.ref("final_helper")
|
||||||
|
return top
|
||||||
|
|
||||||
|
builder.cells.top = cell(make_top)(builder)
|
||||||
|
built = builder.build()
|
||||||
|
report = built.build_report
|
||||||
|
|
||||||
|
assert "final_helper" in built
|
||||||
|
assert "_helper" not in built
|
||||||
|
assert "final_helper" in report.owned_cells["top"]
|
||||||
|
assert "_helper" not in report.owned_cells["top"]
|
||||||
|
prov = report.provenance["final_helper"]
|
||||||
|
assert prov.kind == "helper"
|
||||||
|
assert prov.requested_name == "_helper"
|
||||||
|
assert prov.renamed_from == "_helper"
|
||||||
|
assert prov.final_name == "final_helper"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
|
||||||
|
def make_top(lib: BuildLibrary) -> Pattern:
|
||||||
|
lib["_helper"] = Pattern()
|
||||||
|
del lib["_helper"]
|
||||||
|
return Pattern()
|
||||||
|
|
||||||
|
builder.cells.top = cell(make_top)(builder)
|
||||||
|
built = builder.build()
|
||||||
|
report = built.build_report
|
||||||
|
|
||||||
|
assert "_helper" not in built
|
||||||
|
assert "_helper" not in report.provenance
|
||||||
|
assert report.owned_cells["top"] == ("top",)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_helper_rename_after_auto_rename_preserves_requested_name() -> None:
|
||||||
|
builder = BuildLibrary()
|
||||||
|
|
||||||
|
def make_top(lib: BuildLibrary) -> Pattern:
|
||||||
|
tree = Library({"_helper": Pattern()})
|
||||||
|
_ = lib << tree
|
||||||
|
renamed = lib << tree
|
||||||
|
lib.rename(renamed, "final_helper")
|
||||||
|
top = Pattern()
|
||||||
|
top.ref("_helper")
|
||||||
|
top.ref("final_helper")
|
||||||
|
return top
|
||||||
|
|
||||||
|
builder.cells.top = cell(make_top)(builder)
|
||||||
|
built = builder.build()
|
||||||
|
report = built.build_report
|
||||||
|
|
||||||
|
assert "final_helper" in built
|
||||||
|
prov = report.provenance["final_helper"]
|
||||||
|
assert prov.requested_name == "_helper"
|
||||||
|
assert prov.renamed_from == "_helper"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_rejects_renaming_declared_or_source_cells_during_build() -> None:
|
||||||
|
declared = BuildLibrary()
|
||||||
|
declared["leaf"] = Pattern()
|
||||||
|
|
||||||
|
def rename_declared(lib: BuildLibrary) -> Pattern:
|
||||||
|
lib.rename("leaf", "renamed_leaf")
|
||||||
|
return Pattern()
|
||||||
|
|
||||||
|
declared.cells.top = cell(rename_declared)(declared)
|
||||||
|
with pytest.raises(BuildError, match='Cannot rename declared build cell "leaf"'):
|
||||||
|
declared.build()
|
||||||
|
|
||||||
|
source = BuildLibrary()
|
||||||
|
source.add_source(Library({"src": Pattern()}))
|
||||||
|
|
||||||
|
def rename_source(lib: BuildLibrary) -> Pattern:
|
||||||
|
lib.rename("src", "renamed_src")
|
||||||
|
return Pattern()
|
||||||
|
|
||||||
|
source.cells.top = cell(rename_source)(source)
|
||||||
|
with pytest.raises(BuildError, match='Cannot rename imported source cell "src"'):
|
||||||
|
source.build()
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_rejects_deleting_declared_or_source_cells_during_build() -> None:
|
||||||
|
declared = BuildLibrary()
|
||||||
|
declared["leaf"] = Pattern()
|
||||||
|
|
||||||
|
def delete_declared(lib: BuildLibrary) -> Pattern:
|
||||||
|
del lib["leaf"]
|
||||||
|
return Pattern()
|
||||||
|
|
||||||
|
declared.cells.top = cell(delete_declared)(declared)
|
||||||
|
with pytest.raises(BuildError, match='Cannot delete declared build cell "leaf"'):
|
||||||
|
declared.build()
|
||||||
|
|
||||||
|
source = BuildLibrary()
|
||||||
|
source.add_source(Library({"src": Pattern()}))
|
||||||
|
|
||||||
|
def delete_source(lib: BuildLibrary) -> Pattern:
|
||||||
|
del lib["src"]
|
||||||
|
return Pattern()
|
||||||
|
|
||||||
|
source.cells.top = cell(delete_source)(source)
|
||||||
|
with pytest.raises(BuildError, match='Cannot delete imported source cell "src"'):
|
||||||
|
source.build()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue