8000 [ENH] Propagate exceptions from `cdef` functions by default by shwina · Pull Request #4670 · cython/cython · GitHub
[go: up one dir, main page]

Skip to content

[ENH] Propagate exceptions from cdef functions by default #4670

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

Merged
merged 76 commits into from
Sep 10, 2022

Conversation

shwina
Copy link
Contributor
@shwina shwina commented Feb 28, 2022

Closes #4280

This PR enables Python exceptions to be propagated by (non-extern) cdef functions by default. The behaviour for extern functions is unchanged.

It also adds a new noexcept modifier that can be used to declare cdef functions that will not raise.

Finally, it adds to the language-basics and migrating-to-cython-3.0 parts of the documentation, describing the changes .

Copy link
Contributor
@h-vetinari h-vetinari left a comment

Choose a reason for hiding this comment

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

Just drive-by comments, not actionable until Stefan or David agree.

I would also propose to reshuffle the order of except + and except?, because AFAICT the except + case does not currently do exception checking by default (and assuming this should be kept); that way, the processing would be grouped more consistently, à la:

    exc_check = 1
    if s.sy == 'except':
        if s.sy == '*':
            ...
        elif s.sy == '?':
            ...
### all checked exceptions above this point, all unchecked ones below ###
        elif s.sy == '+':
            ...
        else:
            # case e.g. "except -1"
            ...
    elif s.sy == 'IDENT' and s.systring == 'noexcept':
        ...
    return exc_val, exc_check

@scoder
Copy link
Contributor
scoder commented Mar 3, 2022

I would also propose to reshuffle the order of except + and except?, because AFAICT the except + case does not currently do exception checking by default (and assuming this should be kept); that way, the processing would be grouped more consistently

except -1 and except? -1 seem close variants that don't play in the same league as except * and except +.

@shwina
Copy link
Contributor Author
shwina commented Mar 6, 2022

I'm seeing that a lot of the current failures arise from the fact that the Cython tests and internals define or (2) declare external C functions without an exception value. Presumably, none of those are expected to raise. Due to the change in behaviour introduced in this PR, I'm needing to mark them all noexcept in order to pass tests.

I'm happy to work through all the failures in this way, but wanted to check whether this is an expected side-effect before forging ahead.

@scoder
Copy link
Contributor
scoder commented Mar 6, 2022 via email

@shwina
Copy link
Contributor Author
shwina commented Mar 7, 2022

@scoder It's more than just extern functions (although I will address extern functions in a future comment).

Here's an example; the following program compiles on the main branch, but not this one.

cimport cython                                                                                                                                                                    
                                                                                                                                                                                  
cdef class Foo:                                                                                                                                                                   
    cdef cython.floating method(self, cython.integral x, cython.floating y):                                                                                             
        return x + y                                                                                                                                                              
                                                                                                                                                                                  
cdef Foo obj = Foo()                                                                                                                                                              
                                                                                                                                                                                  
cdef double (*func)(Foo, long, double)                                                                                                                                   
func = obj.method

On this branch, I get:

Error compiling Cython file:
------------------------------------------------------------
...
        return x + y

cdef Foo obj = Foo()

cdef double (*func)(Foo, long, double)
func = obj.method
         ^
------------------------------------------------------------

test.pyx:10:10: Type is not specialized

For reasons I don't fully understand, I have to use noexcept to get the program to compile:

cimport cython                                                                                                                                                                    
                                                                                                                                                                                  
cdef class Foo:                                                                                                                                                                   
    cdef cython.floating method(self, cython.integral x, cython.floating y) noexcept:                                                                                             
        return x + y                                                                                                                                                              
                                                                                                                                                                                  
cdef Foo obj = Foo()                                                                                                                                                              
                                                                                                                                                                                  
cdef double (*func)(Foo, long, double) noexcept                                                                                                                                
func = obj.method

Does this point to a bug somewhere, or would this be an expected result from this PR?

8000

@matusvalo
Copy link
Contributor

For reasons I don't fully understand, I have to use noexcept to get the program to compile:

I think your example makes sense to me to fail since in master branch does not compile following:

cimport cython

cdef class Foo:
    cdef cython.floating method(self, cython.integral x, cython.floating y) except? -1:
        return x + y

cdef Foo obj = Foo()

cdef double (*func)(Foo, long, double) except? -1
func = obj.method

Since we changed default to semantics of except? -1 in this PR, it is correct that this snippet fails now. On the other hand it is correct that you need noexcept keyword since previous default (previous default was equal to noexcept) worked.

Unfortunately, it introduces backward incompatible change but still I think PR works as described in the issue.

@matusvalo
Copy link
Contributor

BTW this PR seems OK for me except comment by @scoder.

@scoder
Copy link
Contributor
scoder commented Mar 17, 2022
test.pyx:10:10: Type is not specialized

This is certainly not the best error message ever. We should at least report that no matching signature was found, and preferably give hints why.

I'm needing to mark them all noexcept in order to pass tests.

This is not how this should work. "extern" functions shouldn't raise exceptions, unless explicitly marked as such. They are just too unlikely to raise in the real world.

@matusvalo
Copy link
Contributor

This is not how this should work. "extern" functions shouldn't raise exceptions, unless explicitly marked as such. They are just too unlikely to raise in the real world.

Right now it is not clear how should be "extern" functions handled. Even bug report leaves it as open questions:

Questions:

  • non-extern cdef functions declared in .pxd files should probably be affected, too, since they should match the implementation in the corresponding .pyx / .py file.
  • C functions defined in cdef extern blocks are unlikely to benefit from this change. Can we live with keeping them different?

Since they are unlikely to raise exceptions, should be marked as noexcept or they should be defined without this keyword?

Similarly how we should threat pointer to function:

  • should be marked as noexcept explicitly or not?
  • Do we want to support pointer to function raising exception in the future?

@scoder
Copy link
Contributor
scoder commented Mar 19, 2022

pointer to function

That seems the biggest open question. As soon as we make a distinction between extern and non-extern functions, we end up with the problem that function pointers need to be able to refer to both. And they seem equally likely to refer to either kind of function, maybe even with a slightly higher chance of pointing to Cython implemented (i.e. non-extern) functions, even though they are a totally C-ish thing.

So you'd end up with code like this:

cdef extern from "someheader.h":
    int cfunc(int x)

cdef int cdeffunc(int x) noexcept:
    return x + 1

cdef int (*func)(int x) noexcept

func = cfunc
func = cdeffunc

and

cdef extern from "someheader.h":
    int cfunc(int x) except? -1

cdef int cdeffunc(int x):
    return x + 1

cdef int (*func)(int x)

func = cfunc
func = cdeffunc

OTOH, if we make both behave the same, then we break pretty much all .pxd files out there, but end up with something more consistent:

cdef extern from "someheader.h" noexcept:
    int cfunc(int x)

cdef int cdeffunc(int x) noexcept:
    return x + 1

cdef int (*func)(int x) noexcept

func = cfunc
func = cdeffunc

It feels like the decision is between "easy to use (in most cases)" and "easy to explain".

@da-woods
Copy link
Contributor

If we went for "consistent" (i.e. require noexcept everywhere) then we might want to backport noexcept to 0.29.x (but just ignore it) so that people can at least write files that work on both

Co-authored-by: h-vetinari <h.vetinari@gmx.com>
@shwina
Copy link
Contributor Author
shwina commented Aug 25, 2022

That horse has long left the barn, but it would be more intuitive to me (aside from jibing with except *) if except were to check for exceptions and except? didn't ("?" -> more uncertainty...).

I don't fully understand -- is this proposing except? as an alternative to noexcept?

@h-vetinari
Copy link
Contributor
h-vetinari commented Aug 25, 2022

I don't fully understand -- is this proposing except? as an alternative to noexcept?

No, sorry for the confusion. I was just musing that the hypothetical

clause exc_check exc_val
except * True None
except <val> True <val>
except? <val> False <val>

would be more intuitive to me ("?" signifying more uncertainty / less checking) -- aside from also avoiding the inconsistency of except * having a different exc_check-behaviour than except <val> -- than the current

clause exc_check exc_val
except * True None
except <val> False <val>
except? <val> True <val>

But of course, we cannot change (much less invert!) the meaning of except vs. except?. 🥲

@shwina
Copy link
Contributor Author
shwina commented Aug 26, 2022

@scoder @da-woods @h-vetinari Could I ask for what I hope would be a final review?

Copy link
Contributor
@da-woods da-woods left a comment

Choose a reason for hiding this comment

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

This looks good I think. The few comments I have are minor ones.

I'd definitely like to wait for @scoder to go over it too. It's a fairly significant change (in terms of the effect it'll have) so more eyes on it is probably better.

But I don't see any reason for this not to make the next release.

"""
Parse exception value clause.

Maps clauses to exc_check / exc_value / exc_clause as follows:
Copy link
Contributor

Choose a reason for hiding this comment

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

exc_clause is just for the nogil order warning I think? That's fine, but I think it'd be good to mention that in the comment. Just because exc_check and exc_value are much more important to how Cython handles exceptions and I think it's worth making that clear

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While it's true that currently exc_clause is only used to correctly raise the nogil warning, it could potentially be useful for other things in the future? I just feel it's very easy for the comment here to become out of sync with how the return value is actually used outside this function.

Copy link
Contributor

The comment contains a table with a rather complete specification. If anything at all changes in the implementation, it will most likely need to be adapted. I don't think more information will make that any worse.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Added a note.

@scoder scoder 8000 added enhancement Python Semantics General Python (3) language semantics labels Sep 7, 2022
@scoder scoder added this to the 3.0 milestone Sep 7, 2022
Copy link
Contributor
@scoder scoder left a comment

Choose a reason for hiding this comment

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

Looks good to me now. Just a few doc clarifications.

"""
Parse exception value clause.

Maps clauses to exc_check / exc_value / exc_clause as follows:
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment contains a table with a rather complete specification. If anything at all changes in the implementation, it will most likely need to be adapted. I don't think more information will make that any worse.

scoder and others added 3 commits September 7, 2022 09:48
Co-authored-by: da-woods <dw-git@d-woods.co.uk>
@da-woods da-woods merged commit 77918c5 into cython:master Sep 10, 2022
@da-woods
Copy link
Contributor

Thanks @shwina - this should remove one of the last major blocks for 3.0

@0dminnimda
Copy link
Contributor

#4936 seems important / affecting not a small percentage of users, while other blockers seems to be pretty narrow, so should #4936 be the last one before 3.0 or is it fine to postpone it?

@da-woods
Copy link
Contributor

I'd forgotten about #4936... I'm not sure. I'll have a look and see how difficult it seems. At very least we should get a warning into Cython 3 about the future change of behaviour

@tacaswell
Copy link
Contributor

This PR broke scipy's build, bisected back to 77918c5 specifically.

Not sure if the right place to fix this is cython or scipy. Do you want a new issue on the cython side? scipy/scipy#17234 is the issue on scipy.

bisect log + script folded.

✔ 15:11:53 $ git bisect log
git bisect start
# status: waiting for both good and bad commits
# good: [9366abc7d6700da7c98b3b1f169e4f2bfac28c54] Fix code style.
git bisect good 9366abc7d6700da7c98b3b1f169e4f2bfac28c54
# status: waiting for bad commit, 1 good commit known
# bad: [c900b6a587801aa3efc918809e53d6c33bc8ed73] always set CYTHON_UPDATE_DESCRIPTOR_DOC to 0 on PyPy (#5083)
git bisect bad c900b6a587801aa3efc918809e53d6c33bc8ed73
# good: [a30eba6f6ea2fffe4a9670c3403f17e416bf2227] CI: Prevent changes to irrelevant files from triggering a CI run.
git bisect good a30eba6f6ea2fffe4a9670c3403f17e416bf2227
# bad: [44b64546e6c90801bf8dc279d6cf2e33b02e4ed2] Set explicit permissions for GitHub Workflows (GH-5038)
git bisect bad 44b64546e6c90801bf8dc279d6cf2e33b02e4ed2
# good: [de2ca1746ba7c8a4c373005d4c6ed02aba3c1978] ci-run.sh: run msvc build with multiple processes (GH-4977)
git bisect good de2ca1746ba7c8a4c373005d4c6ed02aba3c1978
# bad: [490d3ebaf17fb3ad369cfd913d31de902324f184] Use proper SPDX identifier (GH-5032)
git bisect bad 490d3ebaf17fb3ad369cfd913d31de902324f184
# bad: [77918c57b5f36bee708f625e2499bf05b23a87f1] [ENH] Propagate exceptions from `cdef` functions by default (#4670)
git bisect bad 77918c57b5f36bee708f625e2499bf05b23a87f1
# good: [866e1a01fab85ef9ae5cf0be9a75ffaac9b9e104] Make sure we call __del__ for final types (#4996)
git bisect good 866e1a01fab85ef9ae5cf0be9a75ffaac9b9e104
# good: [606bd8cf235149c3be6876d0f5ae60032c8aab6c] Try to use test_dataclass from CPython (#4955)
git bisect good 606bd8cf235149c3be6876d0f5ae60032c8aab6c
# first bad commit: [77918c57b5f36bee708f625e2499bf05b23a87f1] [ENH] Propagate exceptions from `cdef` functions by default (#4670)
#! /usr/bin/xonsh

import argparse
from xonsh.dirstack import with_pushd
from pathlib import Path

$RAISE_SUBPROC_ERROR = True

git clean -xfd
pip install -v . --no-build-isolation

with with_pushd('/home/tcaswell/source/p/scipy/scipy'):
    git clean -xfd
    pip install -v . --no-build-isolation

@da-woods
Copy link
Contributor

I've replied on the Scipy issue but I think we can probably fix this in the Cython PyCapsule definitions. Needs a bit more thought though

@da-woods
Copy link
Contributor

Replied again on the Scipy issue (scipy/scipy#17234 (comment)). To summarise: with a more detailed look I think the right fix is to annotate a cdef function is Scipy with noexcept and that this is probably an expected consequence of this change.

@dalcinl
Copy link
Member
dalcinl commented Oct 16, 2022

@da-woods @scoder I'm updating mpi4py for Cython 3, and this change hit me relatively hard. Although I can easily manage to update all of my codebase quickly and without too much pain, other users with large Cython codebases may suffer a lot. I actually agree with the change and I'm all for it. However, IMHO this change is a huge backward-compatibility fiasco. Perhaps we should consider adding a -X cmdline option and/or per-file compiler directive to let users switch to previous semantics.

@da-woods
Copy link
Contributor

-X command line options and per-file directives are the same thing I believe so if we do one we do both.

I definitely don't think we want to make it anything more granular than "per-file". And currently the work for this is done in the parser so that pretty much forces it to be done per-file file.

I can see this being a big change for some codebases so I can see a switch being helpful (much as we don't like too many switches because they add combinations that need testing). So I wouldn't be opposed to this.

@matusvalo
Copy link
Contributor

So I wouldn't be opposed to this.

I took liberty and created an issue for that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Python Semantics General Python (3) language semantics
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[ENH] C functions should propagate exceptions by default
8 participants
0