From 2a264d6f525f5fbd6c06f52c580d955fce515746 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sat, 18 Oct 2025 02:19:56 -0400 Subject: [PATCH 01/11] [mypyc] fix: UnboundLocalError incorrectly raised as AttributeError fixes https://github.com/mypyc/mypyc/issues/1151 I'm not entirely sure what the difference is between emit_attribute_error and the code block that I edited I'm not sure if we need to update both cases, but I know we do need to handle it in this one --- mypyc/codegen/emitfunc.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index a1e18353693e..6826ef305dac 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -434,14 +434,21 @@ def visit_get_attr(self, op: GetAttr) -> None: merged_branch = branch self.emitter.emit_line("}") if not merged_branch: - exc_class = "PyExc_AttributeError" - self.emitter.emit_line( - 'PyErr_SetString({}, "attribute {} of {} undefined");'.format( - exc_class, - repr(op.attr.removeprefix(GENERATOR_ATTRIBUTE_PREFIX)), - repr(cl.name), + var_name = op.attr.removeprefix(GENERATOR_ATTRIBUTE_PREFIX) + if cl.is_generated: + # A generated class does not "exist" to the user, this is just an unbound + # variable in their code, not a missing attribute on the generated class. + # NOTE We are safe to use the more specific UnboundLocalError here because + # we know that NameError type cases won't compile. + exc_class = "PyExc_UnboundLocalError" + self.emitter.emit_line( + f'PyErr_SetString({exc_class}, "local variable {var_name!r} referenced before assignment");' + ) + else: + exc_class = "PyExc_AttributeError" + self.emitter.emit_line( + f'PyErr_SetString({exc_class}, "attribute {var_name!r} of {cl.name!r} undefined");' ) - ) if attr_rtype.is_refcounted and not op.is_borrowed: if not merged_branch and not always_defined: From 9bd471470717db3a8a208e8936572c5f501497d2 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sat, 18 Oct 2025 02:47:19 -0400 Subject: [PATCH 02/11] Update run-generators.test --- mypyc/test-data/run-generators.test | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mypyc/test-data/run-generators.test b/mypyc/test-data/run-generators.test index c8e83173474d..dc68cef19476 100644 --- a/mypyc/test-data/run-generators.test +++ b/mypyc/test-data/run-generators.test @@ -866,7 +866,7 @@ def test_bitmap_is_cleared_when_object_is_reused() -> None: list(gen(True)) # Ensure bitmap has been cleared. - with assertRaises(AttributeError): # TODO: Should be UnboundLocalError + with assertRaises(UnboundLocalError): list(gen(False)) def gen2(set: bool) -> Iterator[int]: @@ -878,7 +878,7 @@ def gen2(set: bool) -> Iterator[int]: def test_undefined_int_in_environment() -> None: list(gen2(True)) - with assertRaises(AttributeError): # TODO: Should be UnboundLocalError + with assertRaises(UnboundLocalError): list(gen2(False)) [case testVariableWithSameNameAsHelperMethod] @@ -902,10 +902,9 @@ def test_same_names() -> None: assert list(gen_send()) == [2] assert list(gen_throw()) == [84] - with assertRaises(AttributeError, "attribute 'send' of 'undefined_gen' undefined"): - # TODO: Should be UnboundLocalError, this test verifies that the attribute name - # matches the variable name in the input code, since internally it's generated - # with a prefix. + with assertRaises(UnboundLocalError, "local variable 'send' referenced before assignment"): + # this test verifies that the attribute name matches the variable name + # in the input code, since internally it's generated with a prefix. list(undefined()) [case testGeneratorInheritance] From 6fd8e507adb9f2f62894b8f57759468778ecb83f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 06:48:40 +0000 Subject: [PATCH 03/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypyc/test-data/run-generators.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/test-data/run-generators.test b/mypyc/test-data/run-generators.test index dc68cef19476..6d35b0c40a1d 100644 --- a/mypyc/test-data/run-generators.test +++ b/mypyc/test-data/run-generators.test @@ -903,7 +903,7 @@ def test_same_names() -> None: assert list(gen_throw()) == [84] with assertRaises(UnboundLocalError, "local variable 'send' referenced before assignment"): - # this test verifies that the attribute name matches the variable name + # this test verifies that the attribute name matches the variable name # in the input code, since internally it's generated with a prefix. list(undefined()) From 77031c3be2306679c27676e665c1713c1e9e990f Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sat, 18 Oct 2025 02:50:50 -0400 Subject: [PATCH 04/11] cleanup --- mypyc/codegen/emitfunc.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index 6826ef305dac..f20a08f7d4a5 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -438,17 +438,12 @@ def visit_get_attr(self, op: GetAttr) -> None: if cl.is_generated: # A generated class does not "exist" to the user, this is just an unbound # variable in their code, not a missing attribute on the generated class. - # NOTE We are safe to use the more specific UnboundLocalError here because - # we know that NameError type cases won't compile. exc_class = "PyExc_UnboundLocalError" - self.emitter.emit_line( - f'PyErr_SetString({exc_class}, "local variable {var_name!r} referenced before assignment");' - ) + exc_msg = f"local variable {var_name!r} referenced before assignment" else: exc_class = "PyExc_AttributeError" - self.emitter.emit_line( - f'PyErr_SetString({exc_class}, "attribute {var_name!r} of {cl.name!r} undefined");' - ) + exc_msg = f"attribute {var_name!r} of {cl.name!r} undefined" + self.emitter.emit_line(f'PyErr_SetString({exc_class}, "{exc_msg}");') if attr_rtype.is_refcounted and not op.is_borrowed: if not merged_branch and not always_defined: From 64ca907a7f3017d8b44a8b9c451ed269a65bd382 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sat, 18 Oct 2025 03:09:02 -0400 Subject: [PATCH 05/11] Update emitfunc.py --- mypyc/codegen/emitfunc.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index f20a08f7d4a5..d2242e7406e0 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -429,7 +429,7 @@ def visit_get_attr(self, op: GetAttr) -> None: ): # Generate code for the following branch here to avoid # redundant branches in the generated code. - self.emit_attribute_error(branch, cl.name, op.attr) + self.emit_attribute_error(branch, cl, op.attr) self.emit_line("goto %s;" % self.label(branch.true)) merged_branch = branch self.emitter.emit_line("}") @@ -937,20 +937,32 @@ def emit_traceback(self, op: Branch) -> None: if op.traceback_entry is not None: self.emitter.emit_traceback(self.source_path, self.module_name, op.traceback_entry) - def emit_attribute_error(self, op: Branch, class_name: str, attr: str) -> None: + def emit_attribute_error(self, op: Branch, class_ir: ClassIR, attr: str) -> None: assert op.traceback_entry is not None globals_static = self.emitter.static_name("globals", self.module_name) - self.emit_line( - 'CPy_AttributeError("%s", "%s", "%s", "%s", %d, %s);' - % ( - self.source_path.replace("\\", "\\\\"), - op.traceback_entry[0], - class_name, - attr.removeprefix(GENERATOR_ATTRIBUTE_PREFIX), - op.traceback_entry[1], - globals_static, + if class_ir.is_generated: + self.emit_line( + 'CPy_UnboundLocalError("%s", "%s", "%s", "%s", %d, %s);' + % ( + self.source_path.replace("\\", "\\\\"), + op.traceback_entry[0], + attr.removeprefix(GENERATOR_ATTRIBUTE_PREFIX), + op.traceback_entry[1], + globals_static, + ) + ) + else: + self.emit_line( + 'CPy_AttributeError("%s", "%s", "%s", "%s", %d, %s);' + % ( + self.source_path.replace("\\", "\\\\"), + op.traceback_entry[0], + class_ir.name, + attr.removeprefix(GENERATOR_ATTRIBUTE_PREFIX), + op.traceback_entry[1], + globals_static, + ) ) - ) if DEBUG_ERRORS: self.emit_line('assert(PyErr_Occurred() != NULL && "failure w/o err!");') From b1548b6f31ca7f81e775f96bb952f99991cd4960 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sat, 18 Oct 2025 03:10:03 -0400 Subject: [PATCH 06/11] Update exc_ops.c --- mypyc/lib-rt/exc_ops.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mypyc/lib-rt/exc_ops.c b/mypyc/lib-rt/exc_ops.c index d8307ecf21f8..6925f6451bb5 100644 --- a/mypyc/lib-rt/exc_ops.c +++ b/mypyc/lib-rt/exc_ops.c @@ -256,4 +256,11 @@ void CPy_AttributeError(const char *filename, const char *funcname, const char * snprintf(buf, sizeof(buf), "attribute '%.200s' of '%.200s' undefined", attrname, classname); PyErr_SetString(PyExc_AttributeError, buf); CPy_AddTraceback(filename, funcname, line, globals); + +void CPy_UnboundLocalError(const char *filename, const char *funcname, const char *attrname, + int line, PyObject *globals) { + char buf[500]; + snprintf(buf, sizeof(buf), "local variable '%.200s' referenced before assignment", attrname); + PyErr_SetString(PyExc_UnboundLocalError, buf); + CPy_AddTraceback(filename, funcname, line, globals); } From e559b4e90e1deeca26f90dfbc700b861270076db Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sat, 18 Oct 2025 03:10:46 -0400 Subject: [PATCH 07/11] Update CPy.h --- mypyc/lib-rt/CPy.h | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index c79923f69e69..d57bf74caedf 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -853,6 +853,8 @@ void CPy_TypeErrorTraceback(const char *filename, const char *funcname, int line PyObject *globals, const char *expected, PyObject *value); void CPy_AttributeError(const char *filename, const char *funcname, const char *classname, const char *attrname, int line, PyObject *globals); +void CPy_UnboundLocalError(const char *filename, const char *funcname, const char *attrname, + int line, PyObject *globals) // Misc operations @@ -962,12 +964,4 @@ static inline int CPyObject_GenericSetAttr(PyObject *self, PyObject *name, PyObj PyObject *CPy_GetName(PyObject *obj); #endif -#if CPY_3_14_FEATURES -void CPy_SetImmortal(PyObject *obj); -#endif - -#ifdef __cplusplus -} -#endif - -#endif // CPY_CPY_H +#if CPY_3_14_FEATURESEx From ff7629aa924ca117ee39609a937fd01694c77099 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sat, 18 Oct 2025 03:12:03 -0400 Subject: [PATCH 08/11] Update CPy.h --- mypyc/lib-rt/CPy.h | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index d57bf74caedf..60a7f6b2ad5e 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -964,4 +964,12 @@ static inline int CPyObject_GenericSetAttr(PyObject *self, PyObject *name, PyObj PyObject *CPy_GetName(PyObject *obj); #endif -#if CPY_3_14_FEATURESEx +#if CPY_3_14_FEATURES +void CPy_SetImmortal(PyObject *obj); +#endif + +#ifdef __cplusplus +} +#endif + +#endif // CPY_CPY_H From 7bc94c7b29c744d7552f812f3952f00015e8926f Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sat, 18 Oct 2025 03:13:26 -0400 Subject: [PATCH 09/11] Update emitfunc.py --- mypyc/codegen/emitfunc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index d2242e7406e0..0c2d874260a5 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -942,7 +942,7 @@ def emit_attribute_error(self, op: Branch, class_ir: ClassIR, attr: str) -> None globals_static = self.emitter.static_name("globals", self.module_name) if class_ir.is_generated: self.emit_line( - 'CPy_UnboundLocalError("%s", "%s", "%s", "%s", %d, %s);' + 'CPy_UnboundLocalError("%s", "%s", "%s", %d, %s);' % ( self.source_path.replace("\\", "\\\\"), op.traceback_entry[0], From c685ecedebc3d12b0a4fcb5d535de0822e3fa699 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sat, 18 Oct 2025 03:19:20 -0400 Subject: [PATCH 10/11] Update CPy.h --- mypyc/lib-rt/CPy.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 60a7f6b2ad5e..754b7935ebad 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -854,7 +854,7 @@ void CPy_TypeErrorTraceback(const char *filename, const char *funcname, int line void CPy_AttributeError(const char *filename, const char *funcname, const char *classname, const char *attrname, int line, PyObject *globals); void CPy_UnboundLocalError(const char *filename, const char *funcname, const char *attrname, - int line, PyObject *globals) + int line, PyObject *globals); // Misc operations From 248dfde733c3feb05e33a134fd5de98f0d3f94a8 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sat, 18 Oct 2025 04:25:01 -0400 Subject: [PATCH 11/11] Update exc_ops.c --- mypyc/lib-rt/exc_ops.c | 1 + 1 file changed, 1 insertion(+) diff --git a/mypyc/lib-rt/exc_ops.c b/mypyc/lib-rt/exc_ops.c index 6925f6451bb5..4a3fec1a9a84 100644 --- a/mypyc/lib-rt/exc_ops.c +++ b/mypyc/lib-rt/exc_ops.c @@ -256,6 +256,7 @@ void CPy_AttributeError(const char *filename, const char *funcname, const char * snprintf(buf, sizeof(buf), "attribute '%.200s' of '%.200s' undefined", attrname, classname); PyErr_SetString(PyExc_AttributeError, buf); CPy_AddTraceback(filename, funcname, line, globals); +} void CPy_UnboundLocalError(const char *filename, const char *funcname, const char *attrname, int line, PyObject *globals) {