diff --git a/guppylang/engine.py b/guppylang/engine.py index 595d3bcfc..6fbb7c619 100644 --- a/guppylang/engine.py +++ b/guppylang/engine.py @@ -100,6 +100,17 @@ def register_impl(self, ty_id: DefId, name: str, impl_id: DefId) -> None: frame = self.frames[impl_id].f_back if frame: self.frames[impl_id] = frame + # For Python 3.12 generic functions and classes, there is an additional + # inserted frame for the annotation scope. We can detect this frame by + # looking for the special ".generic_base" variable in the frame locals + # that is implicitly inserted by CPython. See + # - https://docs.python.org/3/reference/executionmodel.html#annotation-scopes + # - https://docs.python.org/3/reference/compound_stmts.html#generic-functions + # - https://jellezijlstra.github.io/pep695.html + if ".generic_base" in frame.f_locals: + frame = frame.f_back + assert frame is not None + self.frames[impl_id] = frame DEF_STORE: DefinitionStore = DefinitionStore() diff --git a/tests/integration/test_poly_py312.py b/tests/integration/test_poly_py312.py index 601a5cf54..f4043c9bd 100644 --- a/tests/integration/test_poly_py312.py +++ b/tests/integration/test_poly_py312.py @@ -28,6 +28,24 @@ def main(s: MyStruct[int, float]) -> float: validate(guppy.compile(main)) +def test_inner_frame(validate): + """See https://github.com/CQCL/guppylang/issues/1116""" + def make(): + @guppy.struct + class MyStruct[T]: + @guppy + def foo(self: "MyStruct[int]") -> None: + pass + + @guppy + def main() -> None: + MyStruct[int]().foo() + + return main + + validate(guppy.compile(make())) + + def test_copy_bound(validate): @guppy.struct class MyStruct[T: Copy]: