Skip to content

Commit f2b1a58

Browse files
authored
Refactor so builder context only stores Blocks (#432)
Having context be any Refable was overkill and didn't do anything except for one clone check, This changes builder context to only store Blocks (including Links), and has the context push happen in the init metaclass. Other changes: - Renames ConstraintExpr.parent to ConstraintExpr._context, since parent in Block and Port refer to a binding, whereas the Constraint binding is stored in the Binding object. - Changes the `__init__` hook to unconditionally pass through args and kwargs, instead of looking for *args, **kwargs. - Prevents builder double-pushing the same context. - Ensure the dummy instantiation of part libraries are in lambdas, otherwise they were being created on program start
1 parent 0634299 commit f2b1a58

File tree

11 files changed

+83
-74
lines changed

11 files changed

+83
-74
lines changed

edg/core/Blocks.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -407,35 +407,32 @@ def _bind_in_place(self, parent: Union[BaseBlock, Port]):
407407
def _bind(self: SelfType, parent: Union[BaseBlock, Port]) -> SelfType:
408408
"""Returns a clone of this object with the specified binding. This object must be unbound."""
409409
assert self._parent is None, "can't clone bound block"
410-
assert builder.get_curr_context() is self._lexical_parent, "can't clone to different context"
410+
assert builder.get_enclosing_block() is self._block_context, "can't clone to different context"
411411
clone = type(self)(*self._initializer_args[0], **self._initializer_args[1]) # type: ignore
412412
clone._bind_in_place(parent)
413413
return clone
414414

415415
def _check_constraint(self, constraint: ConstraintExpr) -> None:
416416
def check_subexpr(expr: Union[ConstraintExpr, BasePort]) -> None: # TODO rewrite this whole method
417417
if isinstance(expr, ConstraintExpr) and isinstance(expr.binding, ParamBinding):
418-
if isinstance(expr.parent, BaseBlock):
419-
block_parent = expr.parent
420-
elif isinstance(expr.parent, BasePort):
421-
block_parent = cast(BaseBlock, expr.parent._block_parent()) # TODO make less ugly
422-
assert block_parent is not None
423-
else:
424-
raise ValueError(f"unknown parent {expr.parent} of {expr}")
425-
426-
if isinstance(block_parent._parent, BasePort):
427-
block_parent_parent: Any = block_parent._parent._block_parent()
428-
else:
429-
block_parent_parent = block_parent._parent
430-
431-
if not (block_parent is self or block_parent_parent is self):
432-
raise UnreachableParameterError(f"In {type(self)}, constraint references unreachable parameter {expr}. "
418+
expr_parent = expr.binding.parent
419+
if isinstance(expr_parent, BasePort):
420+
expr_block_parent = expr_parent._block_parent()
421+
assert expr_block_parent is not None
422+
expr_parent = expr_block_parent
423+
424+
expr_parent_parent = expr_parent._parent # may be None for top-level
425+
if isinstance(expr_parent_parent, BasePort): # resolve Link parent port to parent block
426+
expr_parent_parent = expr_parent_parent._block_parent()
427+
428+
if not (expr_parent is self or expr_parent_parent is self):
429+
raise UnreachableParameterError(f"In {self}, constraint references unreachable parameter {expr}. "
433430
"Only own parameters, or immediate contained blocks' parameters can be accessed.")
434431
elif isinstance(expr, BasePort):
435432
block_parent = cast(BaseBlock, expr._block_parent())
436433
assert block_parent is not None
437434
if not block_parent is self or block_parent._parent is self:
438-
raise UnreachableParameterError(f"In {type(self)}, constraint references unreachable port {expr}. "
435+
raise UnreachableParameterError(f"In {self}, constraint references unreachable port {expr}. "
439436
"Only own ports, or immediate contained blocks' ports can be accessed.")
440437

441438
for subexpr in constraint._get_exprs():

edg/core/Builder.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,44 @@
22

33
from typing import *
44

5+
from deprecated import deprecated
6+
57
from .. import edgir
68

79
if TYPE_CHECKING:
8-
from .Core import Refable
910
from .Blocks import BaseBlock
1011

1112

1213
class Builder:
1314
def __init__(self) -> None:
14-
self.stack: List[Refable] = []
15+
self.stack: List[BaseBlock] = []
1516

16-
def push_element(self, elt: Refable) -> None:
17-
self.stack.append(elt)
17+
def push_element(self, elt: BaseBlock) -> Optional[BaseBlock]:
18+
"""Pushes a new element onto the context stack, returning the previous top element.
19+
Ignores if the element is already on top of the stack."""
20+
prev_elt = self.get_enclosing_block()
21+
if not self.stack or self.stack[-1] is not elt: # prevent double-pushing
22+
self.stack.append(elt)
23+
return prev_elt
1824

19-
def pop_to(self, elt: Optional[Refable]) -> None:
20-
self.stack.pop()
21-
assert self.get_curr_context() is elt
25+
def pop_to(self, elt: Optional[BaseBlock]) -> None:
26+
while (elt is None and self.stack) or (elt is not None and self.stack[-1] is not elt):
27+
self.stack.pop()
2228

2329
def get_enclosing_block(self) -> Optional[BaseBlock]:
24-
from .Blocks import BaseBlock
25-
# traverse down the stack and get the first BaseBlock, ignoring things like ports
26-
for elt in reversed(self.stack):
27-
if isinstance(elt, BaseBlock):
28-
return elt
29-
return None
30-
31-
def get_curr_context(self) -> Optional[Refable]:
3230
if not self.stack:
3331
return None
3432
else:
3533
return self.stack[-1]
3634

35+
@deprecated("use get_enclosing_block() instead, context frames can only be blocks now")
36+
def get_curr_context(self) -> Optional[BaseBlock]:
37+
return self.get_enclosing_block()
38+
3739
def elaborate_toplevel(self, block: BaseBlock, *,
3840
is_generator: bool = False,
3941
generate_values: Iterable[Tuple[edgir.LocalPath, edgir.ValueLit]] = []) -> edgir.HierarchyBlock:
40-
assert self.get_curr_context() is None
42+
assert self.get_enclosing_block() is None
4143
self.push_element(block)
4244
try:
4345
if is_generator: # TODO this is kind of nasty =(

edg/core/ConstraintExpr.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
if TYPE_CHECKING:
1919
from .Ports import BasePort
20+
from .Blocks import BaseBlock
2021

2122

2223
SelfType = TypeVar('SelfType', bound='ConstraintExpr')
@@ -64,7 +65,7 @@ def __init__(self: SelfType, initializer: Optional[Union[SelfType, WrappedType]]
6465
self.initializer = None
6566
else:
6667
self.initializer = self._to_expr_type(initializer)
67-
self.parent = builder.get_curr_context()
68+
self._context: Optional[BaseBlock] = builder.get_enclosing_block()
6869

6970
def _get_exprs(self) -> Iterable[Union[ConstraintExpr, BasePort]]:
7071
assert self.binding is not None
@@ -80,7 +81,7 @@ def _new_bind(cls: Type[SelfType], binding: Binding) -> SelfType:
8081
def _bind(self: SelfType, binding: Binding) -> SelfType:
8182
"""Returns a clone of this object with the specified binding. This object must be unbound."""
8283
assert not self._is_bound()
83-
assert builder.get_curr_context() is self.parent, f"can't clone in original context {self.parent} to different new context {builder.get_curr_context()}"
84+
assert builder.get_enclosing_block() is self._context, f"can't clone in original context {self._context} to different new context {builder.get_enclosing_block()}"
8485
if not isinstance(binding, ParamBinding):
8586
assert self.initializer is None, "Only Parameters may have initializers"
8687
clone: SelfType = type(self)(self.initializer)

edg/core/Core.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -146,17 +146,12 @@ class ElementMeta(type):
146146
"""Hook on construction to store some metadata about its creation.
147147
This hooks the top-level __init__ only."""
148148
def __call__(cls, *args, **kwargs):
149-
parent = builder.get_curr_context()
150149
block_context = builder.get_enclosing_block()
151-
try:
152-
obj = type.__call__(cls, *args, **kwargs)
153-
obj._initializer_args = (args, kwargs) # stores args so it is clone-able
154-
obj._lexical_parent = parent # stores context for error checking
155-
obj._block_context = block_context
156-
obj._post_init()
157-
finally:
158-
if builder.get_curr_context() is not parent: # in case the constructor skipped internal element init
159-
builder.pop_to(parent)
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()
160155

161156
return obj
162157

@@ -198,12 +193,10 @@ def __repr__(self) -> str:
198193
return "%s@%02x" % (self._get_def_name(), (id(self) // 4) & 0xff)
199194

200195
def __init__(self) -> None:
201-
self._lexical_parent: Optional[LibraryElement] # set by metaclass
196+
self._block_context: Optional["Refable"] # set by metaclass, as lexical scope available pre-binding
202197
self._parent: Optional[LibraryElement] = None # set by binding, None means not bound
203198
self._initializer_args: Tuple[Tuple[Any, ...], Dict[str, Any]] # set by metaclass
204199

205-
builder.push_element(self)
206-
207200
self.manager = SubElementManager()
208201
self.manager_ignored: Set[str] = set(['_parent'])
209202

edg/core/HierarchyBlock.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,11 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Any:
147147
arg_data: List[Tuple[str, inspect.Parameter, Type[ConstraintExpr]]] = []
148148
# discard param 0 (self)
149149
for arg_name, arg_param in list(inspect.signature(orig_init).parameters.items())[1:]:
150-
if arg_param.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
151-
param_expr_type = BlockMeta._ANNOTATION_EXPR_MAP.get(arg_param.annotation, None)
152-
if param_expr_type is None:
153-
raise BlockDefinitionError(new_cls, f"in {new_cls}.__init__, unknown annotation type for {arg_name}: {arg_param.annotation}")
154-
else:
155-
param_expr_type = ConstraintExpr # dummy placeholder
150+
if arg_param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
151+
continue # pass through of *args, **kwargs handled later
152+
param_expr_type = BlockMeta._ANNOTATION_EXPR_MAP.get(arg_param.annotation, None)
153+
if param_expr_type is None:
154+
raise BlockDefinitionError(new_cls, f"in {new_cls}.__init__, unknown annotation type for {arg_name}: {arg_param.annotation}")
156155

157156
arg_data.append((arg_name, arg_param, param_expr_type))
158157

@@ -182,21 +181,13 @@ def remap_arg(arg_name: str, arg_type: Type[ConstraintExpr], arg_value: Any) ->
182181

183182
return arg_type()._bind(InitParamBinding(self, typed_arg_value))
184183

185-
# create wrapper ConstraintExpr in new object scope
186-
builder_prev = builder.get_curr_context()
187-
builder.push_element(self)
184+
builder_prev = builder.push_element(self)
188185
try:
189186
# rebuild args and kwargs by traversing the args list
190187
new_args: List[Any] = []
191188
new_kwargs: Dict[str, Any] = {}
192189
for arg_pos, (arg_name, arg_param, param_expr_type) in enumerate(arg_data):
193-
if arg_param.kind == inspect.Parameter.VAR_POSITIONAL: # pass-through *args, handled at lower level
194-
new_args.extend(args[arg_pos:])
195-
elif arg_param.kind == inspect.Parameter.VAR_KEYWORD: # pass-through *kwargs, handled at lower level
196-
for arg_name in kwargs:
197-
if arg_name not in new_kwargs:
198-
new_kwargs[arg_name] = kwargs[arg_name]
199-
elif arg_pos < len(args) and arg_param.kind in (inspect.Parameter.POSITIONAL_ONLY,
190+
if arg_pos < len(args) and arg_param.kind in (inspect.Parameter.POSITIONAL_ONLY,
200191
inspect.Parameter.POSITIONAL_OR_KEYWORD): # present positional arg
201192
new_arg = remap_arg(arg_name, param_expr_type, args[arg_pos])
202193
new_args.append(new_arg)
@@ -221,11 +212,17 @@ def remap_arg(arg_name: str, arg_type: Type[ConstraintExpr], arg_value: Any) ->
221212
new_arg = remap_arg(arg_name, param_expr_type, None)
222213
new_kwargs[arg_name] = new_arg
223214
self._init_params[arg_name] = new_arg
215+
216+
# unconditionally pass through all args and kwargs
217+
new_args.extend(args[len(new_args):])
218+
for arg_name in kwargs:
219+
if arg_name not in new_kwargs:
220+
new_kwargs[arg_name] = kwargs[arg_name]
221+
222+
orig_init(self, *new_args, **new_kwargs)
224223
finally:
225224
builder.pop_to(builder_prev)
226225

227-
orig_init(self, *new_args, **new_kwargs)
228-
229226
new_cls.__init__ = functools.update_wrapper(wrapped_init, orig_init)
230227

231228
return new_cls

edg/core/Link.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,38 @@
11
from __future__ import annotations
22

3+
import functools
34
from typing import *
45

56
from .. import edgir
67
from .Array import BaseVector, DerivedVector
78
from .Blocks import BaseBlock, Connection
8-
from .Core import Refable, non_library
9+
from .Builder import builder
10+
from .Core import Refable, non_library, ElementMeta
911
from .HdlUserExceptions import UnconnectableError
1012
from .IdentityDict import IdentityDict
1113
from .Ports import Port
1214

1315

16+
class LinkMeta(ElementMeta):
17+
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
18+
new_cls = super().__new__(cls, *args, **kwargs)
19+
20+
if '__init__' in new_cls.__dict__:
21+
orig_init = new_cls.__dict__['__init__']
22+
def wrapped_init(self, *args, **kwargs) -> None:
23+
builder_prev = builder.push_element(self)
24+
try:
25+
orig_init(self, *args, **kwargs)
26+
finally:
27+
builder.pop_to(builder_prev)
28+
29+
new_cls.__init__ = functools.update_wrapper(wrapped_init, orig_init)
30+
31+
return new_cls
32+
33+
1434
@non_library
15-
class Link(BaseBlock[edgir.Link]):
35+
class Link(BaseBlock[edgir.Link], metaclass=LinkMeta):
1636
def __init__(self) -> None:
1737
super().__init__()
1838
self.parent: Optional[Port] = None

edg/core/MultipackBlock.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def packed_assign(self, self_param: ConstraintExpr, packed_param: PackedParamTyp
188188
raise BlockDefinitionError(self, "can only define multipack in init")
189189
if isinstance(packed_param, ConstraintExpr):
190190
assert type(self_param) == type(packed_param), "packed_assign parameters must be of the same type"
191-
block_parent = packed_param.parent
191+
block_parent = packed_param._context
192192
assert isinstance(block_parent, Block)
193193
self._packed_assigns_by_packed_block[block_parent][self_param] = packed_param
194194
elif isinstance(packed_param, PackedBlockParamArray):
@@ -234,7 +234,7 @@ def unpacked_assign(self, packed_param: UnpackedParamTypes, self_param: Constrai
234234
raise BlockDefinitionError(self, "can only define multipack in init")
235235
if isinstance(packed_param, ConstraintExpr):
236236
assert type(packed_param) == type(self_param), "unpacked_assign parameters must be of the same type"
237-
block_parent = packed_param.parent
237+
block_parent = packed_param._context
238238
assert isinstance(block_parent, Block)
239239
self._unpacked_assigns_by_packed_block[block_parent][self_param] = packed_param
240240
elif isinstance(packed_param, PackedBlockParam):

edg/core/Ports.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ class BasePort(HasMetadata):
2626
def __init__(self) -> None:
2727
"""Abstract Base Class for ports"""
2828
self._parent: Optional[PortParentTypes] # refined from Optional[Refable] in base LibraryElement
29-
self._block_context: Optional[BaseBlock] # set by metaclass, as lexical scope available pre-binding
3029
self._initializer_args: Tuple[Tuple[Any, ...], Dict[str, Any]] # set by metaclass
3130

3231
super().__init__()

edg/jlcparts/JlcPartsDiode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,4 @@ def _entry_to_table_row(cls, row_dict: Dict[PartsTableColumn, Any], filename: st
7070
return None
7171

7272

73-
lambda: JlcPartsDiode(), JlcPartsZenerDiode() # ensure class is instantiable (non-abstract)
73+
lambda: (JlcPartsDiode(), JlcPartsZenerDiode()) # ensure class is instantiable (non-abstract)

edg/jlcparts/JlcPartsFet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,4 @@ class JlcPartsSwitchFet(PartsTableSelectorFootprint, JlcPartsBaseFet, TableSwitc
6868
pass
6969

7070

71-
lambda: JlcPartsFet(), JlcPartsSwitchFet() # ensure class is instantiable (non-abstract)
71+
lambda: (JlcPartsFet(), JlcPartsSwitchFet()) # ensure class is instantiable (non-abstract)

0 commit comments

Comments
 (0)