You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
SystemError for pep695 type parameter with the same name as the inner class #109219
The name_4 in the body of the inner class should refer to the type param of the outer class, because the bodies of inner classes can't see names defined in outer classes, but they can see names defined in outer function-like scopes.
The name_4 in the bases of the inner class should be a NameError. If a name is bound anywhere in a class body, then we look only at the class scope and the global scope, not any intervening scopes. I discuss this in https://jellezijlstra.github.io/pep695 in the section that starts "A few more edge cases came up later." In this case, the name is bound in the class body, but there is no binding yet when the bases of the inner class are evaluated, so next we look at the globals, where there is also no name_4, so we raise a NameError.
cc @carljm, would appreciate if you could see whether you agree with this diagnosis, and if you can see a simple way to implement the desired behavior in the compiler.
I suppose the problem is that inside the annotation scope for the inner class, name_4 needs to be present with two different meanings: a GLOBAL_IMPLICIT for the use in the annotation scope itself, and a free variable, so that it can be put in the cell for the class scope. I'm not sure the compiler has a way to do that in general, though there are some cases involving class scopes that do work like this.
A similar example that still reproduces the bug, but simplifies and perhaps clarifies a few things (replace the outer annotation scope with a regular function scope, and use a normal re-definition of the typevar in the class scope instead of circularly naming the inner class the same as the typevar; this example should run successfully rather than raise a NameError):
def f():
T = str
class C:
T = int
class D[U](T):
T
assert int in C.D.__bases__
In my latter example, the T = str in the outer f() function (equivalent to the presence of name_4 in outer class type params in the original example) is key to reproducing the bug. It is this definition of T (not the redefinition in the class) which causes the innermost T (in the body of the inner class) to have scope FREE rather than GLOBAL_IMPLICIT (as it does if you remove the line T = str from my example.)
I haven't quite sorted out yet what should be happening here; I'll come back to it tomorrow.
I'm using this test case to debug, and focusing on the discrepancy between the behavior with and without the [U] type param on the inner class, since the presence of that type param should not change the behavior of the name T. This test case fails with the SystemError as written, and passes if the [U] is removed:
def test_complex_nested_classes(self):
code = """
def f():
T = str
class C:
T = int
class D[U](T):
x = T
return C
"""
C = run_code(code)["f"]()
self.assertIn(int, C.D.__bases__)
self.assertIs(C.D.x, str)
#109377 fixes this case. I'm still working through some scenarios to see whether it needs more tweaks to avoid breaking any other case.
The fix is to use the symbol flag DEF_CLASS_FREE, which permits adding names to freevars in a class scope, so as to propagate the free var from an inner scope up to a wrapping function-like scope that defines the name in a cell, while the actual uses of the name in the class body can still have a scope other than FREE. So the fix here is to consider a can_see_class_scope scope within a class (i.e. the type param scope here) to be equivalent to a class body when it comes to applying the DEF_CLASS_FREE tag in this situation.
I think the PR may still need an adjustment to ensure we only do this if the name is bound in an enclosing scope.
I used cpython from commit 21f4e6d.
I have no idea if they are all caused by the same bug, but I hope that they are helpful.
classname_1[**name_0]:
try:
classname_3[name_3](name_4=name_0):
name_0except {name_0} asname_5:
fromname_4importname_0else:
passfinally:
pass# output:# Traceback (most recent call last):# File "/home/frank/projects/pysource-playground/test_code.py", line 6, in <module># compile(source,sys.argv[1],"exec")# SystemError: compiler_lookup_arg(name='name_0') with reftype=3 failed in <generic parameters of name_3>; freevars of code name_3: ('.type_params', 'name_0')
classname_1[*name_1]:
asyncdefname_1[**name_0](name_5: name_1, /): # type: name_1# output:# Traceback (most recent call last):# File "/home/frank/projects/pysource-playground/test_code.py", line 6, in <module># compile(source,sys.argv[1],"exec")# SystemError: compiler_lookup_arg(name='name_1') with reftype=3 failed in <generic parameters of name_1>; freevars of code name_1: ('name_1',)
classname_0[name_2]:
(name_2:=name_1)
defname_5[*name_4]() ->name_2: # type: ignoresome textname_2# output:# Traceback (most recent call last):# File "/home/frank/projects/pysource-playground/test_code.py", line 6, in <module># compile(source,sys.argv[1],"exec")# SystemError: compiler_lookup_arg(name='name_2') with reftype=3 failed in <generic parameters of name_5>; freevars of code name_5: ('name_2',)
classname_4[**name_0]:
classname_0[name_4](**name_0):
name_0# output:# Traceback (most recent call last):# File "/home/frank/projects/pysource-playground/test_code.py", line 6, in <module># compile(source,sys.argv[1],"exec")# SystemError: compiler_lookup_arg(name='name_0') with reftype=3 failed in <generic parameters of name_0>; freevars of code name_0: ('.type_params', 'name_0')
classname_5[**name_5]:
classname_5[name_1](**name_5):
{name_4: name_4forname_1inname_3ifname_5}
# output:# Traceback (most recent call last):# File "/home/frank/projects/pysource-playground/test_code.py", line 6, in <module># compile(source,sys.argv[1],"exec")# SystemError: compiler_lookup_arg(name='name_5') with reftype=3 failed in <generic parameters of name_5>; freevars of code name_5: ('.type_params', 'name_5')
classname_0[name_5]:
@(name_5:=0)classname_3[*name_4](name_5=name_5):
{name_0forname_0inname_4ifname_5}
# output:# Traceback (most recent call last):# File "/home/frank/projects/pysource-playground/test_code.py", line 6, in <module># compile(source,sys.argv[1],"exec")# SystemError: compiler_lookup_arg(name='name_5') with reftype=3 failed in <generic parameters of name_3>; freevars of code name_3: ('.type_params', 'name_4', 'name_5')
classname_2[*name_0]:
matchsomething:
case {**name_4}:
name_0=name_3# type: ignoresome textwithname_0: # type: ignoresome textclassname_2[name_5](name_0):
name_0# output:# Traceback (most recent call last):# File "/home/frank/projects/pysource-playground/test_code.py", line 6, in <module># compile(source,sys.argv[1],"exec")# SystemError: compiler_lookup_arg(name='name_0') with reftype=3 failed in <generic parameters of name_2>; freevars of code name_2: ('.type_params', 'name_0')
Background:
I'm currently updating pysource-minimize and pysource-codegen to 3.12.
I created therefor a little test repository https://github.com/15r10nk/pysource-playground, which I used to generate this examples.
This code is still work in progress and the examples might be not as minimal as they could be.
I'm not able to continue my work until next week (family ...).
You can test a new python version with this repository in the meantime if you want. Create an issue in this repository if you have any problems, I will try to help you.
@15r10nk Thank you! At first glance just based on the error messages, I suspect these are all the same bug, but I will turn them into test cases and verify the fix handles all of them.
I tested all of the above cases and #109377 fixes them all; they all compile successfully and then fail as expected at runtime with either NameError or TypeError. I don't think they even need to be added to the PR as separate regression tests, since they all test for the same bug.
Activity
JelleZijlstra commentedon Sep 10, 2023
Thanks. The second one should be fixed by #109123. I'll have to look into what is causing the SystemError on your first example.
JelleZijlstra commentedon Sep 11, 2023
I spent some time last night trying to get this to work without success. I'll try more later today, but here are my thoughts so far on this example:
The
name_4in the body of the inner class should refer to the type param of the outer class, because the bodies of inner classes can't see names defined in outer classes, but they can see names defined in outer function-like scopes.The
name_4in the bases of the inner class should be a NameError. If a name is bound anywhere in a class body, then we look only at the class scope and the global scope, not any intervening scopes. I discuss this in https://jellezijlstra.github.io/pep695 in the section that starts "A few more edge cases came up later." In this case, the name is bound in the class body, but there is no binding yet when the bases of the inner class are evaluated, so next we look at the globals, where there is also noname_4, so we raise a NameError.cc @carljm, would appreciate if you could see whether you agree with this diagnosis, and if you can see a simple way to implement the desired behavior in the compiler.
carljm commentedon Sep 11, 2023
Your analysis looks correct to me! I'll take a look at implementation...
JelleZijlstra commentedon Sep 12, 2023
I suppose the problem is that inside the annotation scope for the inner class,
name_4needs to be present with two different meanings: a GLOBAL_IMPLICIT for the use in the annotation scope itself, and a free variable, so that it can be put in the cell for the class scope. I'm not sure the compiler has a way to do that in general, though there are some cases involving class scopes that do work like this.carljm commentedon Sep 13, 2023
A similar example that still reproduces the bug, but simplifies and perhaps clarifies a few things (replace the outer annotation scope with a regular function scope, and use a normal re-definition of the typevar in the class scope instead of circularly naming the inner class the same as the typevar; this example should run successfully rather than raise a NameError):
carljm commentedon Sep 13, 2023
In my latter example, the
T = strin the outerf()function (equivalent to the presence ofname_4in outer class type params in the original example) is key to reproducing the bug. It is this definition of T (not the redefinition in the class) which causes the innermost T (in the body of the inner class) to have scope FREE rather than GLOBAL_IMPLICIT (as it does if you remove the lineT = strfrom my example.)I haven't quite sorted out yet what should be happening here; I'll come back to it tomorrow.
carljm commentedon Sep 13, 2023
I'm using this test case to debug, and focusing on the discrepancy between the behavior with and without the
[U]type param on the inner class, since the presence of that type param should not change the behavior of the nameT. This test case fails with theSystemErroras written, and passes if the[U]is removed:carljm commentedon Sep 13, 2023
FWIW, I also wrote this
PyST_Dumpfunction to help debug this: https://gist.github.com/carljm/ecf76f8d0b4d77b00a5ae79b93d1418aMaybe we should commit this, ifdef-ed out by default, similar to the
dump_*functions inflowgraph.c.pythongh-109219: propagate used free vars through type param scopes
carljm commentedon Sep 13, 2023
#109377 fixes this case. I'm still working through some scenarios to see whether it needs more tweaks to avoid breaking any other case.
The fix is to use the symbol flag
DEF_CLASS_FREE, which permits adding names tofreevarsin a class scope, so as to propagate the free var from an inner scope up to a wrapping function-like scope that defines the name in a cell, while the actual uses of the name in the class body can still have a scope other thanFREE. So the fix here is to consider acan_see_class_scopescope within a class (i.e. the type param scope here) to be equivalent to a class body when it comes to applying theDEF_CLASS_FREEtag in this situation.I think the PR may still need an adjustment to ensure we only do this if the name is bound in an enclosing scope.
15r10nk commentedon Sep 13, 2023
hi, I generated some more examples.
I used cpython from commit 21f4e6d.
I have no idea if they are all caused by the same bug, but I hope that they are helpful.
Background:
I'm currently updating pysource-minimize and pysource-codegen to 3.12.
I created therefor a little test repository https://github.com/15r10nk/pysource-playground, which I used to generate this examples.
This code is still work in progress and the examples might be not as minimal as they could be.
I'm not able to continue my work until next week (family ...).
You can test a new python version with this repository in the meantime if you want. Create an issue in this repository if you have any problems, I will try to help you.
carljm commentedon Sep 13, 2023
@15r10nk Thank you! At first glance just based on the error messages, I suspect these are all the same bug, but I will turn them into test cases and verify the fix handles all of them.
carljm commentedon Sep 13, 2023
I tested all of the above cases and #109377 fixes them all; they all compile successfully and then fail as expected at runtime with either
NameErrororTypeError. I don't think they even need to be added to the PR as separate regression tests, since they all test for the same bug.pythongh-109219: propagate used free vars through type param scopes [v2]
gh-109219: propagate free vars through type param scopes (#109377)
pythongh-109219: propagate free vars through type param scopes (pytho…
[3.12] gh-109219: propagate free vars through type param scopes (GH-1…
JelleZijlstra commentedon Sep 18, 2023
Merged and backported, thanks @15r10nk for the report and @carljm for the fix!