From 8e0ad301f487c9d3be838bb98f9253e5c7085c21 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 3 Oct 2024 17:14:15 -0400 Subject: [PATCH 1/7] fix[tool]: update VarAccess pickle implementation --- vyper/semantics/analysis/base.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/vyper/semantics/analysis/base.py b/vyper/semantics/analysis/base.py index 65bc8df3ab..7e17e82863 100644 --- a/vyper/semantics/analysis/base.py +++ b/vyper/semantics/analysis/base.py @@ -1,5 +1,5 @@ import enum -from dataclasses import dataclass +from dataclasses import dataclass, field, fields from functools import cached_property from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Union @@ -234,6 +234,17 @@ class VarAccess: # A sentinel indicating a subscript access SUBSCRIPT_ACCESS: ClassVar[Any] = object() + def __reduce__(self): + dict_obj = {f.name: getattr(self, f.name) for f in fields(self)} + return self.__class__._produce, (dict_obj,) + + @classmethod + def _produce(cls, data): + ret = cls.__new__(cls) + for k, v in data.items(): + object.__setattr__(ret, k, v) + return cls + @cached_property def attrs(self): ret = [] @@ -280,6 +291,9 @@ class ExprInfo: modifiability: Modifiability = Modifiability.MODIFIABLE attr: Optional[str] = None + _writes: OrderedSet[VarAccess] = field(default_factory=OrderedSet) + _reads: OrderedSet[VarAccess] = field(default_factory=OrderedSet) + def __post_init__(self): should_match = ("typ", "location", "modifiability") if self.var_info is not None: @@ -287,9 +301,6 @@ def __post_init__(self): if getattr(self.var_info, attr) != getattr(self, attr): raise CompilerPanic(f"Bad analysis: non-matching {attr}: {self}") - self._writes: OrderedSet[VarAccess] = OrderedSet() - self._reads: OrderedSet[VarAccess] = OrderedSet() - @classmethod def from_varinfo(cls, var_info: VarInfo, **kwargs) -> "ExprInfo": return cls( From 22ebb7dbd7e85f8dc81de338a01e41a2ae5eb3e2 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 3 Oct 2024 17:21:07 -0400 Subject: [PATCH 2/7] add a test --- tests/integration/test_pickle_ast.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/integration/test_pickle_ast.py diff --git a/tests/integration/test_pickle_ast.py b/tests/integration/test_pickle_ast.py new file mode 100644 index 0000000000..2c6144603a --- /dev/null +++ b/tests/integration/test_pickle_ast.py @@ -0,0 +1,19 @@ +import copy +import pickle + +from vyper.compiler.phases import CompilerData + + +def test_pickle_ast(): + code = """ +@external +def foo(): + self.bar() + y: uint256 = 5 + x: uint256 = 5 +def bar(): + pass + """ + f = CompilerData(code) + copy.deepcopy(f.annotated_vyper_module) + pickle.loads(pickle.dumps(f.annotated_vyper_module)) From a4d1ae46090389760c26a0b88897c89bf1afd5ba Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 3 Oct 2024 17:24:25 -0400 Subject: [PATCH 3/7] update dict directly --- vyper/semantics/analysis/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vyper/semantics/analysis/base.py b/vyper/semantics/analysis/base.py index 7e17e82863..0aeef3430b 100644 --- a/vyper/semantics/analysis/base.py +++ b/vyper/semantics/analysis/base.py @@ -241,9 +241,9 @@ def __reduce__(self): @classmethod def _produce(cls, data): ret = cls.__new__(cls) - for k, v in data.items(): - object.__setattr__(ret, k, v) - return cls + # bypass blocking of setattr by `frozen=True` + ret.__dict__.update(data) + return ret @cached_property def attrs(self): From 43ab3d0777231717f32da7873452536ba55e10b4 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 4 Oct 2024 15:57:31 -0400 Subject: [PATCH 4/7] use `object.__setattr__()`, it is more general and doesn't require `__dict__` --- vyper/semantics/analysis/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vyper/semantics/analysis/base.py b/vyper/semantics/analysis/base.py index 0aeef3430b..f827448996 100644 --- a/vyper/semantics/analysis/base.py +++ b/vyper/semantics/analysis/base.py @@ -242,7 +242,8 @@ def __reduce__(self): def _produce(cls, data): ret = cls.__new__(cls) # bypass blocking of setattr by `frozen=True` - ret.__dict__.update(data) + for k, v in data.items(): + object.__setattr__(ret, k, v) return ret @cached_property From e1ebcb8a9d439a03c4c3b5917e2a004ec31a4738 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 5 Oct 2024 09:27:22 -0400 Subject: [PATCH 5/7] add a note --- vyper/semantics/analysis/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vyper/semantics/analysis/base.py b/vyper/semantics/analysis/base.py index f827448996..0c64774b47 100644 --- a/vyper/semantics/analysis/base.py +++ b/vyper/semantics/analysis/base.py @@ -234,6 +234,9 @@ class VarAccess: # A sentinel indicating a subscript access SUBSCRIPT_ACCESS: ClassVar[Any] = object() + # custom __reduce__ and _produce implementations to work around + # a pickle bug. + # see https://github.com/python/cpython/issues/124937#issuecomment-2392227290 def __reduce__(self): dict_obj = {f.name: getattr(self, f.name) for f in fields(self)} return self.__class__._produce, (dict_obj,) From d3309e75ca12500d979bbc15f22808bea7f52c12 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 5 Oct 2024 11:37:58 -0400 Subject: [PATCH 6/7] simplify `_produce()` --- vyper/semantics/analysis/base.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/vyper/semantics/analysis/base.py b/vyper/semantics/analysis/base.py index 0c64774b47..f4bf02bc2e 100644 --- a/vyper/semantics/analysis/base.py +++ b/vyper/semantics/analysis/base.py @@ -243,11 +243,7 @@ def __reduce__(self): @classmethod def _produce(cls, data): - ret = cls.__new__(cls) - # bypass blocking of setattr by `frozen=True` - for k, v in data.items(): - object.__setattr__(ret, k, v) - return ret + return cls(**data) @cached_property def attrs(self): From 15efb2ed097cd8609313e1486c40c3a9d3064f08 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 5 Oct 2024 11:54:02 -0400 Subject: [PATCH 7/7] revert VarAccess constructor API change --- vyper/semantics/analysis/base.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vyper/semantics/analysis/base.py b/vyper/semantics/analysis/base.py index f4bf02bc2e..982b6eb01d 100644 --- a/vyper/semantics/analysis/base.py +++ b/vyper/semantics/analysis/base.py @@ -1,5 +1,5 @@ import enum -from dataclasses import dataclass, field, fields +from dataclasses import dataclass, fields from functools import cached_property from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Union @@ -291,15 +291,14 @@ class ExprInfo: modifiability: Modifiability = Modifiability.MODIFIABLE attr: Optional[str] = None - _writes: OrderedSet[VarAccess] = field(default_factory=OrderedSet) - _reads: OrderedSet[VarAccess] = field(default_factory=OrderedSet) - def __post_init__(self): should_match = ("typ", "location", "modifiability") if self.var_info is not None: for attr in should_match: if getattr(self.var_info, attr) != getattr(self, attr): raise CompilerPanic(f"Bad analysis: non-matching {attr}: {self}") + self._writes: OrderedSet[VarAccess] = OrderedSet() + self._reads: OrderedSet[VarAccess] = OrderedSet() @classmethod def from_varinfo(cls, var_info: VarInfo, **kwargs) -> "ExprInfo":