[BuildLibrary] allow prefixing/renaming/postprocessing in BuildCellsView

This commit is contained in:
Jan Petykiewicz 2026-08-28 20:25:14 -07:00
commit 890e7e5c0f
3 changed files with 611 additions and 40 deletions

View file

@ -21,6 +21,7 @@ from ..library import (
)
from ..pattern import Pattern
from ..ports import Port
from ..utils.ports2data import ports_to_data
def _owned_by(report: BuildReport, owner: str) -> set[str]:
@ -832,3 +833,257 @@ def test_build_session_subtree_preserves_type_and_builds_deferred_dependencies()
assert child_calls == 1
assert set(built) == {"top", "child", "leaf", "unrelated"}
def test_build_cells_prefixed_view_generates_facades_for_attributes_and_items() -> None:
builder = LibraryBuilder()
cells = builder.cells.prefixed(
"vendor_",
facade=lambda proxy: ports_to_data(proxy, (10, 2)),
)
cells.fixed = cell(Pattern)(ports={"p": Port((1, 2), 0)})
dynamic_name = "dynamic"
cells[dynamic_name] = cell(Pattern)(ports={"q": Port((3, 4), 0)})
built, report = builder.build(output="library")
assert set(built) == {"vendor_fixed", "fixed", "vendor_dynamic", "dynamic"}
assert set(built["fixed"].refs) == {"vendor_fixed"}
assert set(built["dynamic"].refs) == {"vendor_dynamic"}
assert set(built["fixed"].ports) == {"p"}
assert len(built["fixed"].labels[(10, 2)]) == 1
assert not built["vendor_fixed"].labels
assert report.dependency_graph["fixed"] == frozenset({"vendor_fixed"})
assert report.dependency_graph["dynamic"] == frozenset({"vendor_dynamic"})
def test_build_cells_prefixed_view_postprocesses_only_the_implementation() -> None:
source = Pattern(ports={"p": Port((0, 0), 0)})
builder = LibraryBuilder()
physical = builder.cells.prefixed(
"vendor_",
postprocess=lambda pattern: ports_to_data(pattern, (20, 1)),
)
physical["cell"] = source
unprefixed = builder.cells.prefixed(
None,
postprocess=lambda pattern: ports_to_data(pattern, (21, 1)),
)
unprefixed["local"] = Pattern(ports={"q": Port((1, 1), 0)})
built, _report = builder.build(output="library")
assert set(built) == {"vendor_cell", "local"}
assert len(built["vendor_cell"].labels[(20, 1)]) == 1
assert len(built["local"].labels[(21, 1)]) == 1
assert not source.labels
def test_build_cells_postprocess_precedes_facade_port_copy() -> None:
builder = LibraryBuilder()
def add_port(pattern: Pattern) -> Pattern:
pattern.ports["added"] = Port((5, 6), 0)
return pattern
cells = builder.cells.prefixed(
"impl_",
postprocess=add_port,
facade=lambda pattern: ports_to_data(pattern, (30, 0)),
)
cells.device = Pattern()
built, _report = builder.build(output="library")
assert set(built["impl_device"].ports) == {"added"}
assert set(built["device"].ports) == {"added"}
assert built["device"].labels[(30, 0)][0].string.startswith("added:")
def test_build_cells_facade_is_ignored_when_name_does_not_change() -> None:
builder = LibraryBuilder()
def fail_if_called(_pattern: Pattern) -> Pattern:
raise AssertionError("facade should not be called")
cells = builder.cells.prefixed("vendor_", facade=fail_if_called)
cells["vendor_cell"] = Pattern()
built, _report = builder.build(output="library")
assert set(built) == {"vendor_cell"}
def test_build_cells_renamed_views_are_independent_and_accept_dynamic_names() -> None:
builder = LibraryBuilder()
upper = builder.cells.renamed(str.upper)
suffixed = builder.cells.renamed(lambda name: name + "_v2")
builder.cells.plain = Pattern()
upper["upper"] = Pattern()
suffixed["other"] = Pattern()
built, _report = builder.build(output="library")
assert set(built) == {"plain", "UPPER", "other_v2"}
def test_build_cells_policy_registration_is_atomic() -> None:
builder = LibraryBuilder()
builder.cells.public = Pattern()
prefixed = builder.cells.prefixed("impl_", facade=lambda pattern: pattern)
with pytest.raises(LibraryError, match="already exist"):
prefixed.public = Pattern()
assert set(builder) == {"public"}
assert "impl_public" not in builder
def test_build_cells_policy_views_validate_names_and_transforms() -> None:
builder = LibraryBuilder()
invalid_name = builder.cells.renamed(lambda _name: None) # type: ignore[arg-type,return-value]
with pytest.raises(TypeError, match="expected str"):
invalid_name["cell"] = Pattern()
with pytest.raises(TypeError, match="must be callable"):
builder.cells.prefixed("x_", facade=object()) # type: ignore[arg-type]
assert not builder
def test_build_cells_view_deletion_requires_concrete_names_after_rename() -> None:
builder = LibraryBuilder()
prefixed = builder.cells.prefixed("impl_", facade=lambda pattern: pattern)
prefixed["cell"] = Pattern()
with pytest.raises(BuildError, match="Delete concrete names"):
del prefixed["cell"]
del builder.cells["cell"]
del builder.cells["impl_cell"]
builder.cells["plain"] = Pattern()
del builder.cells["plain"]
assert not builder
def test_build_cells_item_reads_remain_write_only() -> None:
builder = LibraryBuilder()
builder.cells["cell"] = Pattern()
with pytest.raises(BuildError, match="write-only"):
_ = builder.cells["cell"]
def test_build_cells_prefixed_view_normalizes_recipe_tree_and_generates_facade() -> None:
builder = LibraryBuilder()
builder.cells["_helper"] = Pattern()
cells = builder.cells.prefixed(
"impl_",
postprocess=lambda pattern: Pattern(ports={"out": Port((7, 8), 0)}, refs=pattern.refs),
facade=lambda pattern: ports_to_data(pattern, (40, 0)),
)
def make_tree() -> Library:
tree = Library({"_helper": Pattern(), "old_top": Pattern()})
tree["old_top"].ref("_helper")
return tree
cells.tree = cell(make_tree)()
built, report = builder.build(output="library")
assert {"_helper", "impl_tree", "tree"} <= set(built)
helper_targets = set(built["impl_tree"].refs)
assert "_helper" not in helper_targets
assert len(helper_targets) == 1
renamed_helper = helper_targets.pop()
assert renamed_helper in built
assert report.provenance[renamed_helper].kind == "helper"
assert report.provenance[renamed_helper].requested_name == "_helper"
assert report.provenance[renamed_helper].owner_declared_name == "impl_tree"
assert set(built["tree"].ports) == {"out"}
assert set(built["tree"].refs) == {"impl_tree"}
def test_build_cells_view_accepts_direct_tree_and_rejects_multiple_tops() -> None:
builder = LibraryBuilder()
builder.cells["tree"] = Library({"old_top": Pattern()})
built, _report = builder.build(output="library")
assert set(built) == {"tree"}
invalid = LibraryBuilder()
invalid.cells["tree"] = cell(lambda: Library({"one": Pattern(), "two": Pattern()}))()
with pytest.raises(BuildError, match="exactly one topcell"):
invalid.validate()
def test_build_cells_tree_declared_name_wins_over_single_use_helper() -> None:
builder = LibraryBuilder()
def make_tree() -> Library:
top = Pattern()
top.ref("_device")
return Library({"_device": Pattern(), "old_top": top})
builder.cells["_device"] = cell(make_tree)()
built, report = builder.build(output="library")
assert "_device" in built
assert set(built["_device"].refs) != {"_device"}
assert len(built["_device"].refs) == 1
helper_name = next(iter(built["_device"].refs))
assert helper_name in built
assert report.provenance["_device"].kind == "declared"
assert report.provenance["_device"].requested_name == "_device"
assert report.provenance[helper_name].kind == "helper"
assert report.provenance[helper_name].requested_name == "_device"
assert report.provenance[helper_name].owner_declared_name == "_device"
def test_build_cells_tree_declared_name_rejects_public_helper_conflict() -> None:
builder = LibraryBuilder()
def make_tree() -> Library:
top = Pattern()
top.ref("device")
return Library({"device": Pattern(), "old_top": top})
builder.cells["device"] = cell(make_tree)()
with pytest.raises(BuildError, match="Unresolved duplicate key"):
builder.validate()
del builder.cells["device"]
builder.cells["device"] = Pattern()
builder.validate()
def test_build_cells_replacement_postprocess_updates_self_published_recipe_and_facade() -> None:
builder = LibraryBuilder()
cells = builder.cells.prefixed(
"impl_",
postprocess=lambda _pattern: Pattern(ports={"new": Port((5, 6), 0)}),
facade=lambda pattern: ports_to_data(pattern, (50, 0)),
)
def make_device(lib: ILibrary) -> Pattern:
original = Pattern(ports={"old": Port((1, 2), 0)})
lib["impl_device"] = original
return original
cells.device = cell(make_device)(builder.library)
built, report = builder.build(output="library")
assert set(built) == {"impl_device", "device"}
assert set(built["impl_device"].ports) == {"new"}
assert set(built["device"].ports) == {"new"}
assert built["device"].labels[(50, 0)][0].string.startswith("new:")
assert report.provenance["impl_device"].kind == "declared"
assert report.dependency_graph["device"] == frozenset({"impl_device"})
def test_build_cells_rejects_unrelated_self_published_pattern() -> None:
builder = LibraryBuilder()
def make_device(lib: ILibrary) -> Pattern:
lib["device"] = Pattern()
return Pattern()
builder.cells.device = cell(make_device)(builder.library)
with pytest.raises(BuildError, match="wrote a different pattern"):
builder.validate()