Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Break reference cycles in the edge triggers #727

Merged
merged 2 commits into from Dec 17, 2018

Conversation

eric-wieser
Copy link
Member

Noticed in the comments of #723.

Rationale:
* ClockCycles shares only one line of implementation with _Edge, and it's easier just to repeat it.
* ClockCycles doesn't make much sense as a subclass of _Edge
* _Edge is private anyway, so no consumer of the public API will care
_RisingOrFallingEdge.__init__(self, signal, rising=False)

class Edge(_EdgeBase):
""" Triggers on either edge in a signal """
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a bonus, this now gives these names docstrings, where previously help(cocotb.triggers.FallingEdge) would give nothing

Previously there was a cyclic reference between `Signal._r_edge -> _Edge.signal -> ...`

This takes the same approach as 94eeaba,
using a weak dictionary to ensure edges keep their signal alive, but not
vice versa.
@imphil imphil merged commit 9e17862 into cocotb:master Dec 17, 2018
@eric-wieser
Copy link
Member Author

Thanks @imphil for the fast approval!

@eric-wieser eric-wieser deleted the edge-weakref branch December 18, 2018 04:47
eric-wieser added a commit to eric-wieser/cocotb that referenced this pull request Dec 20, 2018
…by a misunderstanding of how __new__ works

In those patches, I declared classes that were roughly
```python
class SomeClass:
    def __new__(cls, arg):
        try:
            return existing[arg]
        except KeyError:
            return super(SomeClass, cls).__new__(cls, arg)

    def __init__(self, arg):
        self.arg = arg
        self.state = 0
```

This approach has a fatal flaw (cocotbgh-729), with function calls shown in the following code:
```python
A = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A
# A.__init__(1)
B = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A   # reusing the existing instance
# A.__init__(1)  # uh oh, we reset A.state
```

We need to override class-construction without allowing `__init__` to run a second time.

One option would be to just remove `__init__` entirely, and move the contents into `__new__`.
The other option, which I take in this patch, is to introduce a metaclass overriding the `__call__` operator on class types.

I'm not convinced this is the best approach, but it does fix the problem.
eric-wieser added a commit to eric-wieser/cocotb that referenced this pull request Dec 20, 2018
…by a misunderstanding of how __new__ works

In those patches, I declared classes that were roughly
```python
class SomeClass:
    def __new__(cls, arg):
        try:
            return existing[arg]
        except KeyError:
            return super(SomeClass, cls).__new__(cls, arg)

    def __init__(self, arg):
        self.arg = arg
        self.state = 0
```

This approach has a fatal flaw (cocotbgh-729), with function calls shown in the following code:
```python
A = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A
# A.__init__(1)
B = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A   # reusing the existing instance
# A.__init__(1)  # uh oh, we reset A.state
```

We need to override class-construction without allowing `__init__` to run a second time.

This introduces a simple metaclass to allow overriding the behavior of `RisingEdge.__call__(...)`
eric-wieser added a commit to eric-wieser/cocotb that referenced this pull request Jan 7, 2019
…by a misunderstanding of how __new__ works

In those patches, I declared classes that were roughly
```python
class SomeClass:
    def __new__(cls, arg):
        try:
            return existing[arg]
        except KeyError:
            return super(SomeClass, cls).__new__(cls, arg)

    def __init__(self, arg):
        self.arg = arg
        self.state = 0
```

This approach has a fatal flaw (cocotbgh-729), with function calls shown in the following code:
```python
A = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A
# A.__init__(1)
B = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A   # reusing the existing instance
# A.__init__(1)  # uh oh, we reset A.state
```

We need to override class-construction without allowing `__init__` to run a second time.

This introduces a simple metaclass to allow overriding the behavior of `RisingEdge.__call__(...)`
eric-wieser added a commit to eric-wieser/cocotb that referenced this pull request Jan 7, 2019
…by a misunderstanding of how __new__ works

In those patches, I declared classes that were roughly
```python
class SomeClass:
    def __new__(cls, arg):
        try:
            return existing[arg]
        except KeyError:
            return super(SomeClass, cls).__new__(cls, arg)

    def __init__(self, arg):
        self.arg = arg
        self.state = 0
```

This approach has a fatal flaw (cocotbgh-729), with function calls shown in the following code:
```python
A = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A
# A.__init__(1)
B = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A   # reusing the existing instance
# A.__init__(1)  # uh oh, we reset A.state
```

We need to override class-construction without allowing `__init__` to run a second time.

One option would be to just remove `__init__` entirely, and move the contents into `__new__`.
The other option, which I take in this patch, is to introduce a metaclass overriding the `__call__` operator on class types.

I'm not convinced this is the best approach, but it does fix the problem.
eric-wieser added a commit to eric-wieser/cocotb that referenced this pull request Jan 7, 2019
…by a misunderstanding of how __new__ works

In those patches, I declared classes that were roughly
```python
class SomeClass:
    def __new__(cls, arg):
        try:
            return existing[arg]
        except KeyError:
            return super(SomeClass, cls).__new__(cls, arg)

    def __init__(self, arg):
        self.arg = arg
        self.state = 0
```

This approach has a fatal flaw (cocotbgh-729), with function calls shown in the following code:
```python
A = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A
# A.__init__(1)
B = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A   # reusing the existing instance
# A.__init__(1)  # uh oh, we reset A.state
```

We need to override class-construction without allowing `__init__` to run a second time.

This introduces a simple metaclass to allow overriding the behavior of `RisingEdge.__call__(...)`
eric-wieser added a commit to eric-wieser/cocotb that referenced this pull request Jan 7, 2019
…by a misunderstanding of how __new__ works

In those patches, I declared classes that were roughly
```python
class SomeClass:
    def __new__(cls, arg):
        try:
            return existing[arg]
        except KeyError:
            return super(SomeClass, cls).__new__(cls, arg)

    def __init__(self, arg):
        self.arg = arg
        self.state = 0
```

This approach has a fatal flaw (cocotbgh-729), with function calls shown in the following code:
```python
A = SomeClass(1)
B = SomeClass(1)
```

We need to override class-construction without allowing `__init__` to run a second time.

One option would be to just remove `__init__` entirely, and move the contents into `__new__`.
The other option, which I take in this patch, is to introduce a metaclass overriding the `__call__` operator on class types.

I'm not convinced this is the best approach, but it does fix the problem.
eric-wieser added a commit to eric-wieser/cocotb that referenced this pull request Jan 7, 2019
…by a misunderstanding of how __new__ works

In those patches, I declared classes that were roughly
```python
class SomeClass:
    def __new__(cls, arg):
        try:
            return existing[arg]
        except KeyError:
            return super(SomeClass, cls).__new__(cls, arg)

    def __init__(self, arg):
        self.arg = arg
        self.state = 0
```

This approach has a fatal flaw (cocotbgh-729), with function calls shown in the following code:
```python
A = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A
# A.__init__(1)
B = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A   # reusing the existing instance
# A.__init__(1)  # uh oh, we reset A.state
```

For the sake of simplicity, this commit undoes the merger between the factory functions and the classes themselves, while keeping the weakref behavior.

In future, it would be good to explore the more complex approach taken in cocotbgh-733, which ensures that `isinstance(Join(), Join)`, which this patch does not.
Since this property was not true before cocotbgh-723 and cocotbgh-727 anyway, we can afford to revert that improvement in order to get a release out the door.
eric-wieser added a commit to eric-wieser/cocotb that referenced this pull request Jan 8, 2019
…by a misunderstanding of how __new__ works

In those patches, I declared classes that were roughly
```python
class SomeClass:
    def __new__(cls, arg):
        try:
            return existing[arg]
        except KeyError:
            return super(SomeClass, cls).__new__(cls, arg)

    def __init__(self, arg):
        self.arg = arg
        self.state = 0
```

This approach has a fatal flaw (cocotbgh-729), with function calls shown in the following code:
```python
A = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A
# A.__init__(1)
B = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A   # reusing the existing instance
# A.__init__(1)  # uh oh, we reset A.state
```

For the sake of simplicity, this commit undoes the merger between the factory functions and the classes themselves, while keeping the weakref behavior.

In future, it would be good to explore the more complex approach taken in cocotbgh-733, which ensures that `isinstance(Join(), Join)`, which this patch does not.
Since this property was not true before cocotbgh-723 and cocotbgh-727 anyway, we can afford to revert that improvement in order to get a release out the door.
eric-wieser added a commit to eric-wieser/cocotb that referenced this pull request Jan 8, 2019
…by a misunderstanding of how __new__ works

In those patches, I declared classes that were roughly
```python
class SomeClass:
    def __new__(cls, arg):
        try:
            return existing[arg]
        except KeyError:
            return super(SomeClass, cls).__new__(cls, arg)

    def __init__(self, arg):
        self.arg = arg
        self.state = 0
```

This approach has a fatal flaw (cocotbgh-729), with function calls shown in the following code:
```python
A = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A
# A.__init__(1)
B = SomeClass(1)
# SomeClass.__new__(SomeClass, 1) -> A   # reusing the existing instance
# A.__init__(1)  # uh oh, we reset A.state
```

For the sake of simplicity, this commit undoes the merger between the factory functions and the classes themselves, while keeping the weakref behavior.

In future, it would be good to explore the more complex approach taken in cocotbgh-733, which ensures that `isinstance(Join(), Join)`, which this patch does not.
Since this property was not true before cocotbgh-723 and cocotbgh-727 anyway, we can afford to revert that improvement in order to get a release out the door.
imphil added a commit that referenced this pull request Jan 8, 2019
Fix a regression introduced in gh-727 and gh-723, caused by a misunderstanding of how __new__ works
ktbarrett pushed a commit to ktbarrett/cocotb that referenced this pull request Mar 6, 2020
 Break reference cycles in the edge triggers
ktbarrett pushed a commit to ktbarrett/cocotb that referenced this pull request Mar 6, 2020
…by a misunderstanding of how __new__ works

In those patches, I declared classes that were roughly
```python
class SomeClass:
    def __new__(cls, arg):
        try:
            return existing[arg]
        except KeyError:
            return super(SomeClass, cls).__new__(cls, arg)

    def __init__(self, arg):
        self.arg = arg
        self.state = 0
```

This approach has a fatal flaw (cocotbgh-729), with function calls shown in the following code:
```python
A = SomeClass(1)
B = SomeClass(1)
```

We need to override class-construction without allowing `__init__` to run a second time.

One option would be to just remove `__init__` entirely, and move the contents into `__new__`.
The other option, which I take in this patch, is to introduce a metaclass overriding the `__call__` operator on class types.

I'm not convinced this is the best approach, but it does fix the problem.
ktbarrett pushed a commit to ktbarrett/cocotb that referenced this pull request Mar 6, 2020
Fix a regression introduced in cocotbgh-727 and cocotbgh-723, caused by a misunderstanding of how __new__ works
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants