diff --git a/masque/builder/pather.py b/masque/builder/pather.py index a7bbedd..b4c264d 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -16,7 +16,7 @@ from numpy import pi from numpy.typing import ArrayLike from ..pattern import Pattern -from ..library import ILibrary, TreeView +from ..library import ILibrary, TreeView, SINGLE_USE_PREFIX from ..error import BuildError, PortError from ..ports import PortList, Port from ..abstract import Abstract @@ -1067,9 +1067,24 @@ class Pather(PortList): tool_port_names = ('A', 'B') pat = Pattern() + def validate_tree(portspec: str, batch: list[RenderStep], tree: ILibrary) -> None: + missing = sorted( + name + for name in tree.dangling_refs(tree.top()) + if isinstance(name, str) and name.startswith(SINGLE_USE_PREFIX) + ) + if not missing: + return + + tool_name = type(batch[0].tool).__name__ + raise BuildError( + f'Tool {tool_name}.render() returned missing single-use refs for {portspec}: {missing}' + ) + def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None: assert batch[0].tool is not None tree = batch[0].tool.render(batch, port_names=tool_port_names) + validate_tree(portspec, batch, tree) name = self.library << tree if portspec in pat.ports: del pat.ports[portspec] diff --git a/masque/builder/tools.py b/masque/builder/tools.py index 44318d1..5fc6943 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -451,7 +451,9 @@ class Tool: else: continue - pat.plug(seg_tree.top_pattern(), {port_names[1]: port_names[0]}, append=True) + seg_name = lib << seg_tree + pat.plug(lib[seg_name], {port_names[1]: port_names[0]}, append=True) + del lib[seg_name] return lib diff --git a/masque/test/test_pather_api.py b/masque/test/test_pather_api.py index 5f84101..d01ed44 100644 --- a/masque/test/test_pather_api.py +++ b/masque/test/test_pather_api.py @@ -684,6 +684,123 @@ def test_pather_uturn_failed_fallback_is_atomic() -> None: assert len(p.paths['A']) == 0 +def test_pather_render_auto_renames_single_use_tool_children() -> None: + class FullTreeTool(Tool): + def planL(self, ccw, length, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202 + ptype = out_ptype or in_ptype or 'wire' + return Port((length, 0), rotation=pi, ptype=ptype), {'length': length} + + def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202 + tree = Library() + top = Pattern(ports={ + port_names[0]: Port((0, 0), 0, ptype='wire'), + port_names[1]: Port((1, 0), pi, ptype='wire'), + }) + child = Pattern(annotations={'batch': [len(batch)]}) + top.ref('_seg') + tree['_top'] = top + tree['_seg'] = child + return tree + + lib = Library() + p = Pather(lib, tools=FullTreeTool(), auto_render=False) + p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') + + p.straight('A', 10) + p.render() + p.straight('A', 10) + p.render() + + assert len(lib) == 2 + assert set(lib.keys()) == set(p.pattern.refs.keys()) + assert len(set(p.pattern.refs.keys())) == 2 + assert all(name.startswith('_seg') for name in lib) + assert p.pattern.referenced_patterns() <= set(lib.keys()) + + +def test_tool_render_fallback_preserves_segment_subtrees() -> None: + class TraceTreeTool(Tool): + def traceL(self, ccw, length, *, in_ptype=None, out_ptype=None, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001 + tree = Library() + top = Pattern(ports={ + port_names[0]: Port((0, 0), 0, ptype='wire'), + port_names[1]: Port((length, 0), pi, ptype='wire'), + }) + child = Pattern(annotations={'length': [length]}) + top.ref('_seg') + tree['_top'] = top + tree['_seg'] = child + return tree + + lib = Library() + p = Pather(lib, tools=TraceTreeTool(), auto_render=False) + p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') + + p.straight('A', 10) + p.render() + + assert '_seg' in lib + assert '_seg' in p.pattern.refs + assert p.pattern.referenced_patterns() <= set(lib.keys()) + + +def test_pather_render_rejects_missing_single_use_tool_refs() -> None: + class MissingSingleUseTool(Tool): + def planL(self, ccw, length, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202 + ptype = out_ptype or in_ptype or 'wire' + return Port((length, 0), rotation=pi, ptype=ptype), {'length': length} + + def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202 + tree = Library() + top = Pattern(ports={ + port_names[0]: Port((0, 0), 0, ptype='wire'), + port_names[1]: Port((1, 0), pi, ptype='wire'), + }) + top.ref('_seg') + tree['_top'] = top + return tree + + lib = Library() + lib['_seg'] = Pattern(annotations={'stale': [1]}) + p = Pather(lib, tools=MissingSingleUseTool(), auto_render=False) + p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') + p.straight('A', 10) + + with pytest.raises(BuildError, match='missing single-use refs'): + p.render() + + assert list(lib.keys()) == ['_seg'] + assert not p.pattern.refs + + +def test_pather_render_allows_missing_non_single_use_tool_refs() -> None: + class SharedRefTool(Tool): + def planL(self, ccw, length, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202 + ptype = out_ptype or in_ptype or 'wire' + return Port((length, 0), rotation=pi, ptype=ptype), {'length': length} + + def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202 + tree = Library() + top = Pattern(ports={ + port_names[0]: Port((0, 0), 0, ptype='wire'), + port_names[1]: Port((1, 0), pi, ptype='wire'), + }) + top.ref('shared') + tree['_top'] = top + return tree + + lib = Library() + lib['shared'] = Pattern(annotations={'shared': [1]}) + p = Pather(lib, tools=SharedRefTool(), auto_render=False) + p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') + + p.straight('A', 10) + p.render() + + assert 'shared' in p.pattern.refs + assert p.pattern.referenced_patterns() <= set(lib.keys()) + + def test_renderpather_rename_to_none_keeps_pending_geometry_without_port() -> None: lib = Library() tool = PathTool(layer='M1', width=1000)