Skip to content

Commit 26f6bc1

Browse files
authored
BlockPrototype (#435)
Optimization that changes Block model (pre-binding) instantiation to be a BlockPrototype that holds the target block class, args, and kwargs, without running its __init__. The Block __init__ is only run on binding time. BlockPrototype generates error on any attempt to access it. This should not change user-facing HDL. Shaves about a minute off of the test suite. It does not seem possible to apply the same pattern to Ports, since Port model operations in user-facing library HDL do use their data (eg, DigitalSource.from_bidir(model) requires inspecting the model's properties and would leak (more) the prototype mechanics into user-facing HDL. Ports occupy a odd space in that they both contain user HDL (like Blocks, but not ConstraintExpr) and where model components can be used (unlike Blocks, which only allow interaction on bound objects through parameters and ports). Other changes: - Removes _post_init as infrastructure - Removes the post_init elaboration state - Replace Block._bind with BlockPrototype._bind - Removes the ElementMeta metaclass that saves initializer args and context. Initializer args no longer needed for Blocks are are only used for Ports.
1 parent a5e46b0 commit 26f6bc1

File tree

11 files changed

+154
-62
lines changed

11 files changed

+154
-62
lines changed

edg/core/Blocks.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@
2020
from .Link import Link
2121

2222

23+
class BaseBlockMeta(type):
24+
"""Adds a hook to set the post-init elaboration state"""
25+
def __call__(cls, *args, **kwargs):
26+
block_context = builder.get_enclosing_block()
27+
obj = super().__call__(*args, **kwargs)
28+
if isinstance(obj, BaseBlock): # ignore block prototypes
29+
obj._block_context = block_context
30+
return obj
31+
32+
2333
class Connection():
2434
"""An incremental connection builder, that validates additional ports as they are added so
2535
the stack trace can provide the problematic statement."""
@@ -186,7 +196,6 @@ def make_connection(self) -> Optional[Union[ConnectedLink, Export]]:
186196
class BlockElaborationState(Enum):
187197
pre_init = 1 # not sure if this is needed, doesn't actually get used
188198
init = 2
189-
post_init = 3
190199
contents = 4
191200
post_contents = 5
192201
generate = 6
@@ -230,12 +239,13 @@ def set_elt_proto(self, pb, ref_map=None):
230239
AbstractBlockProperty = EltPropertiesBase()
231240

232241
@non_library
233-
class BaseBlock(HasMetadata, Generic[BaseBlockEdgirType]):
242+
class BaseBlock(HasMetadata, Generic[BaseBlockEdgirType], metaclass=BaseBlockMeta):
234243
"""Base block that has ports (IOs), parameters, and constraints between them.
235244
"""
236245
# __init__ should initialize the object with structural information (parameters, fields)
237246
# as well as optionally initialization (parameter defaults)
238247
def __init__(self) -> None:
248+
self._block_context: Optional["Refable"] # set by metaclass, as lexical scope available pre-binding
239249
self._parent: Optional[Union[BaseBlock, Port]] # refined from Optional[Refable] in base LibraryElement
240250

241251
super().__init__()
@@ -276,10 +286,6 @@ def _all_connects_of(self, base: Connection) -> IdentitySet[Connection]:
276286

277287
return delegated_connects
278288

279-
def _post_init(self):
280-
assert self._elaboration_state == BlockElaborationState.init
281-
self._elaboration_state = BlockElaborationState.post_init
282-
283289
def name(self) -> StringExpr:
284290
return self._name
285291

@@ -295,7 +301,7 @@ def _elaborated_def_to_proto(self) -> BaseBlockEdgirType:
295301
prev_element = builder.push_element(self)
296302
assert prev_element is None
297303
try:
298-
assert self._elaboration_state == BlockElaborationState.post_init
304+
assert self._elaboration_state == BlockElaborationState.init
299305
self._elaboration_state = BlockElaborationState.contents
300306
self.contents()
301307
self._elaboration_state = BlockElaborationState.post_contents
@@ -409,15 +415,6 @@ def _get_ref_map(self, prefix: edgir.LocalPath) -> IdentityDict[Refable, edgir.L
409415
def _bind_in_place(self, parent: Union[BaseBlock, Port]):
410416
self._parent = parent
411417

412-
SelfType = TypeVar('SelfType', bound='BaseBlock')
413-
def _bind(self: SelfType, parent: Union[BaseBlock, Port]) -> SelfType:
414-
"""Returns a clone of this object with the specified binding. This object must be unbound."""
415-
assert self._parent is None, "can't clone bound block"
416-
assert builder.get_enclosing_block() is self._block_context, "can't clone to different context"
417-
clone = type(self)(*self._initializer_args[0], **self._initializer_args[1]) # type: ignore
418-
clone._bind_in_place(parent)
419-
return clone
420-
421418
def _check_constraint(self, constraint: ConstraintExpr) -> None:
422419
def check_subexpr(expr: Union[ConstraintExpr, BasePort]) -> None: # TODO rewrite this whole method
423420
if isinstance(expr, ConstraintExpr) and isinstance(expr.binding, ParamBinding):

edg/core/Core.py

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -142,20 +142,6 @@ def values(self) -> ValuesView[ElementType]:
142142
return self.container.values()
143143

144144

145-
class ElementMeta(type):
146-
"""Hook on construction to store some metadata about its creation.
147-
This hooks the top-level __init__ only."""
148-
def __call__(cls, *args, **kwargs):
149-
block_context = builder.get_enclosing_block()
150-
151-
obj = type.__call__(cls, *args, **kwargs)
152-
obj._initializer_args = (args, kwargs) # stores args so it is clone-able
153-
obj._block_context = block_context
154-
obj._post_init()
155-
156-
return obj
157-
158-
159145
class Refable():
160146
"""Object that could be referenced into a edgir.LocalPath"""
161147
def __repr__(self) -> str:
@@ -185,25 +171,19 @@ def non_library(decorated: NonLibraryType) -> NonLibraryType:
185171

186172

187173
@non_library
188-
class LibraryElement(Refable, metaclass=ElementMeta):
174+
class LibraryElement(Refable):
189175
"""Defines a library element, which optionally contains other library elements."""
190176
_elt_properties: Dict[Tuple[Type[LibraryElement], EltPropertiesBase], Any] = {}
191177

192178
def __repr__(self) -> str:
193179
return "%s@%02x" % (self._get_def_name(), (id(self) // 4) & 0xff)
194180

195181
def __init__(self) -> None:
196-
self._block_context: Optional["Refable"] # set by metaclass, as lexical scope available pre-binding
197182
self._parent: Optional[LibraryElement] = None # set by binding, None means not bound
198-
self._initializer_args: Tuple[Tuple[Any, ...], Dict[str, Any]] # set by metaclass
199183

200184
self.manager = SubElementManager()
201185
self.manager_ignored: Set[str] = set(['_parent'])
202186

203-
"""Optionally overloaded to run anything post-__init__"""
204-
def _post_init(self):
205-
pass
206-
207187
def __setattr__(self, name: str, value: Any) -> None:
208188
if hasattr(self, 'manager_ignored') and name not in self.manager_ignored:
209189
self.manager.add_element(name, value)

edg/core/DesignTop.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ def make_packing_refinement(multipack_part: Union[Block, PackedBlockAllocate], p
3636
if isinstance(multipack_part, Block):
3737
return path, type(multipack_part)
3838
elif isinstance(multipack_part, PackedBlockAllocate):
39-
return path, type(multipack_part.parent._tpe)
39+
assert multipack_part.parent._elt_sample is not None
40+
return path, type(multipack_part.parent._elt_sample)
4041
else:
4142
raise TypeError
4243

@@ -55,7 +56,7 @@ def _elaborated_def_to_proto(self) -> edgir.HierarchyBlock:
5556
prev_element = builder.push_element(self)
5657
assert prev_element is None
5758
try:
58-
assert self._elaboration_state == BlockElaborationState.post_init
59+
assert self._elaboration_state == BlockElaborationState.init
5960
self._elaboration_state = BlockElaborationState.contents
6061
self.contents()
6162
self.multipack()

edg/core/Generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def _generated_def_to_proto(self, generate_values: Iterable[Tuple[edgir.LocalPat
111111
assert prev_element is None
112112

113113
try:
114-
assert self._elaboration_state == BlockElaborationState.post_init # TODO dedup w/ elaborated_def_to_proto
114+
assert self._elaboration_state == BlockElaborationState.init
115115
self._elaboration_state = BlockElaborationState.contents
116116
self.contents()
117117
self._elaboration_state = BlockElaborationState.generate

edg/core/HierarchyBlock.py

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
ArrayFloatLike, ArrayRangeLike, ArrayStringLike
1313
from .Array import BaseVector, Vector
1414
from .Binding import InitParamBinding, AssignBinding
15-
from .Blocks import BaseBlock, Connection, BlockElaborationState, AbstractBlockProperty
15+
from .Blocks import BaseBlock, Connection, BlockElaborationState, AbstractBlockProperty, BaseBlockMeta
1616
from .ConstraintExpr import BoolLike, FloatLike, IntLike, RangeLike, StringLike
1717
from .ConstraintExpr import ConstraintExpr, BoolExpr, FloatExpr, IntExpr, RangeExpr, StringExpr
18-
from .Core import Refable, non_library, ElementMeta
18+
from .Core import Refable, non_library
1919
from .HdlUserExceptions import *
2020
from .IdentityDict import IdentityDict
2121
from .IdentitySet import IdentitySet
@@ -100,7 +100,41 @@ def __iter__(self):
100100
return iter((tuple(self.blocks), self))
101101

102102

103-
class BlockMeta(ElementMeta):
103+
BlockPrototypeType = TypeVar('BlockPrototypeType', bound='Block')
104+
class BlockPrototype(Generic[BlockPrototypeType]):
105+
"""A block prototype, that contains a type and arguments, but without constructing the entire block
106+
and running its (potentially quite expensive) __init__.
107+
108+
This class is automatically created on Block instantiations by the BlockMeta metaclass __init__ hook."""
109+
def __init__(self, tpe: Type[BlockPrototypeType], args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> None:
110+
self._tpe = tpe
111+
self._args = args
112+
self._kwargs = kwargs
113+
114+
def __repr__(self) -> str:
115+
return f"{self.__class__.__name__}({self._tpe}, args={self._args}, kwargs={self._kwargs})"
116+
117+
def _bind(self, parent: Union[BaseBlock, Port]) -> BlockPrototypeType:
118+
"""Binds the prototype into an actual Block instance."""
119+
Block._next_bind = self._tpe
120+
block = self._tpe(*self._args, **self._kwargs) # type: ignore
121+
block._bind_in_place(parent)
122+
return block
123+
124+
def __getattribute__(self, item: str) -> Any:
125+
if item.startswith("_"):
126+
return super().__getattribute__(item)
127+
else:
128+
raise AttributeError(f"{self.__class__.__name__} has no attributes, must bind to get a concrete instance, tried to get {item}")
129+
130+
def __setattr__(self, key: str, value: Any) -> None:
131+
if key.startswith("_"):
132+
super().__setattr__(key, value)
133+
else:
134+
raise AttributeError(f"{self.__class__.__name__} has no attributes, must bind to get a concrete instance, tried to set {key}")
135+
136+
137+
class BlockMeta(BaseBlockMeta):
104138
"""This provides a hook on __init__ that replaces argument values with empty ConstraintExpr
105139
based on the type annotation and stores the supplied argument to the __init__ (if any) in the binding.
106140
@@ -233,6 +267,23 @@ class Block(BaseBlock[edgir.HierarchyBlock], metaclass=BlockMeta):
233267
"""Part with a statically-defined subcircuit.
234268
Relations between contained parameters may only be expressed in the given constraint language.
235269
"""
270+
_next_bind: Optional[Type[Block]] = None # set when binding, to avoid creating a prototype
271+
272+
def __new__(cls, *args: Any, **kwargs: Any) -> Block:
273+
if Block._next_bind is not None:
274+
assert Block._next_bind is cls
275+
Block._next_bind = None
276+
return super().__new__(cls)
277+
elif builder.get_enclosing_block() is None: # always construct if top-level
278+
return super().__new__(cls)
279+
else:
280+
return BlockPrototype(cls, args, kwargs) # type: ignore
281+
282+
SelfType = TypeVar('SelfType', bound='BaseBlock')
283+
def _bind(self: SelfType, parent: Union[BaseBlock, Port]) -> SelfType:
284+
# for type checking only
285+
raise TypeError("_bind should be called from BlockPrototype")
286+
236287
def __init__(self) -> None:
237288
super().__init__()
238289

@@ -410,14 +461,19 @@ def _def_to_proto(self) -> edgir.HierarchyBlock:
410461
def with_mixin(self, tpe: MixinType) -> MixinType:
411462
"""Adds an interface mixin for this Block. Mainly useful for abstract blocks, e.g. IoController with HasI2s."""
412463
from .BlockInterfaceMixin import BlockInterfaceMixin
413-
if not (isinstance(tpe, BlockInterfaceMixin) and tpe._is_mixin()):
464+
if isinstance(tpe, BlockPrototype):
465+
tpe_cls = tpe._tpe
466+
else:
467+
tpe_cls = tpe.__class__
468+
469+
if not (issubclass(tpe_cls, BlockInterfaceMixin) and tpe_cls._is_mixin()):
414470
raise TypeError("param to with_mixin must be a BlockInterfaceMixin")
415471
if isinstance(self, BlockInterfaceMixin) and self._is_mixin():
416472
raise BlockDefinitionError(self, "mixins can not have with_mixin")
417473
if (self.__class__, AbstractBlockProperty) not in self._elt_properties:
418474
raise BlockDefinitionError(self, "mixins can only be added to abstract classes")
419-
if not isinstance(self, tpe._get_mixin_base()):
420-
raise TypeError(f"block {self.__class__.__name__} not an instance of mixin base {tpe._get_mixin_base().__name__}")
475+
if not isinstance(self, tpe_cls._get_mixin_base()):
476+
raise TypeError(f"block {self.__class__.__name__} not an instance of mixin base {tpe_cls._get_mixin_base().__name__}")
421477
assert self._parent is not None
422478

423479
elt = tpe._bind(self._parent)
@@ -569,16 +625,23 @@ def Export(self, port: ExportType, tags: Iterable[PortTag]=[], *, optional: bool
569625
def Block(self, tpe: BlockType) -> BlockType:
570626
from .BlockInterfaceMixin import BlockInterfaceMixin
571627
from .DesignTop import DesignTop
572-
if not isinstance(tpe, Block):
573-
raise TypeError(f"param to Block(...) must be Block, got {tpe} of type {type(tpe)}")
574-
if isinstance(tpe, BlockInterfaceMixin) and tpe._is_mixin():
575-
raise TypeError("param to Block(...) must not be BlockInterfaceMixin")
576-
if isinstance(tpe, DesignTop):
577-
raise TypeError(f"param to Block(...) may not be DesignTop")
628+
578629
if self._elaboration_state not in \
579-
[BlockElaborationState.init, BlockElaborationState.contents, BlockElaborationState.generate]:
630+
[BlockElaborationState.init, BlockElaborationState.contents, BlockElaborationState.generate]:
580631
raise BlockDefinitionError(self, "can only define blocks in init, contents, or generate")
581632

633+
if isinstance(tpe, BlockPrototype):
634+
tpe_cls = tpe._tpe
635+
else:
636+
tpe_cls = tpe.__class__
637+
638+
if not issubclass(tpe_cls, Block):
639+
raise TypeError(f"param to Block(...) must be Block, got {tpe_cls}")
640+
if issubclass(tpe_cls, BlockInterfaceMixin) and tpe_cls._is_mixin():
641+
raise TypeError("param to Block(...) must not be BlockInterfaceMixin")
642+
if issubclass(tpe_cls, DesignTop):
643+
raise TypeError(f"param to Block(...) may not be DesignTop")
644+
582645
elt = tpe._bind(self)
583646
self._blocks.register(elt)
584647

edg/core/Link.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55

66
from .. import edgir
77
from .Array import BaseVector, DerivedVector
8-
from .Blocks import BaseBlock, Connection
8+
from .Blocks import BaseBlock, Connection, BaseBlockMeta
99
from .Builder import builder
10-
from .Core import Refable, non_library, ElementMeta
10+
from .Core import Refable, non_library
1111
from .HdlUserExceptions import UnconnectableError
1212
from .IdentityDict import IdentityDict
1313
from .Ports import Port
1414

1515

16-
class LinkMeta(ElementMeta):
16+
class LinkMeta(BaseBlockMeta):
1717
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
1818
new_cls = super().__new__(cls, *args, **kwargs)
1919

edg/core/MultipackBlock.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .Core import non_library, SubElementDict
1313
from .ConstraintExpr import ConstraintExpr, BoolExpr, IntExpr, FloatExpr, RangeExpr, StringExpr
1414
from .Ports import BasePort, Port
15-
from .HierarchyBlock import Block
15+
from .HierarchyBlock import Block, BlockPrototype
1616

1717

1818
class PackedBlockAllocate(NamedTuple):
@@ -134,7 +134,12 @@ def __init__(self):
134134
def PackedPart(self, tpe: PackedPartType) -> PackedPartType:
135135
"""Adds a block type that can be packed into this block.
136136
The block is a "virtual block" that will not appear in the design tree."""
137-
if not isinstance(tpe, (Block, PackedBlockArray)):
137+
if isinstance(tpe, BlockPrototype):
138+
tpe_cls = tpe._tpe
139+
else:
140+
tpe_cls = tpe.__class__
141+
142+
if not issubclass(tpe_cls, (Block, PackedBlockArray)):
138143
raise TypeError(f"param to PackedPart(...) must be Block, got {tpe} of type {type(tpe)}")
139144
if self._elaboration_state != BlockElaborationState.init:
140145
raise BlockDefinitionError(self, "can only define multipack in init")

edg/core/Ports.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,26 @@
1818
from .PortBlocks import PortBridge, PortAdapter
1919

2020

21+
class InitializerContextMeta(type):
22+
def __call__(cls, *args, **kwargs):
23+
"""Hook on construction to store some metadata about its creation.
24+
This hooks the top-level __init__ only."""
25+
obj = type.__call__(cls, *args, **kwargs)
26+
obj._initializer_args = (args, kwargs) # stores args so it is clone-able
27+
return obj
28+
29+
2130
PortParentTypes = Union['BaseContainerPort', 'BaseBlock']
2231
@non_library
23-
class BasePort(HasMetadata):
32+
class BasePort(HasMetadata, metaclass=InitializerContextMeta):
2433
SelfType = TypeVar('SelfType', bound='BasePort')
2534

2635
def __init__(self) -> None:
2736
"""Abstract Base Class for ports"""
2837
self._parent: Optional[PortParentTypes] # refined from Optional[Refable] in base LibraryElement
38+
self._block_context: Optional["Refable"] # set by metaclass, as lexical scope available pre-binding
2939
self._initializer_args: Tuple[Tuple[Any, ...], Dict[str, Any]] # set by metaclass
40+
self._block_context = builder.get_enclosing_block()
3041

3142
super().__init__()
3243

@@ -157,12 +168,15 @@ def init_from(self: SelfType, other: SelfType):
157168

158169
def _bridge(self) -> Optional[PortBridge]:
159170
"""Creates a (unbound) bridge and returns it."""
171+
from .HierarchyBlock import Block
172+
160173
if self.bridge_type is None:
161174
return None
162175
if self._bridge_instance is not None:
163176
return self._bridge_instance
164177
assert self._is_bound(), "not bound, can't create bridge"
165178

179+
Block._next_bind = self.bridge_type
166180
self._bridge_instance = self.bridge_type()
167181
return self._bridge_instance
168182

edg/core/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .MultiBiDict import MultiBiDict
2222

2323
# Features for library builders
24-
from .Core import LibraryElement, SubElementDict, ElementDict, ElementMeta, non_library
24+
from .Core import LibraryElement, SubElementDict, ElementDict, non_library
2525
from .Blocks import BasePort, BaseBlock
2626
from .Categories import InternalBlock
2727
from .Builder import builder

0 commit comments

Comments
 (0)