GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — bpo-46841: Don't jump during `throw()` by brandtbucher · Pull Request #31968 · python/cpython

Conversation

@brandtbucher

@brandtbucher brandtbucher commented Mar 17, 2022 •
edited by bedevere-bot
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

We currently perform some awkward control flow in _gen_throw to handle the case where a yield from/await sub-iterator returns a value during a throw() call.

The current code is quite sensitive to slight changes in the bytecode, and is technically invalid for three reasons:

  • It doesn't properly handle the case where SEND has an EXTENDED_ARG.
  • It scans backwards, which is susceptible to accidentally reading inline cache entries as if they were valid instructions.
  • It jumps from a YIELD_VALUE instruction, which isn't really supposed to be possible.

Instead, this PR handles this edge-case by pushing NULL and the return value in place of the sub-iterator and its next value. SEND then handles this by adjusting the stack and jumping the same way it does when handling other return values.

This also updates the docs for SEND (which are currently lacking a bit).

https://bugs.python.org/issue46841

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
brandtbucher added 4 commits March 17, 2022 11:33
@brandtbucher
Handle return-during-throw in SEND
6ee3f38
@brandtbucher
Update the docs for SEND
dcf97b2
@brandtbucher
blurb add
fb649c7
@brandtbucher
Clean things up a bit
acdf984
@brandtbucher
brandtbucher requested review from 1st1 and markshannon as code owners March 17, 2022 20:15
@bedevere-bot bedevere-bot added the awaiting core review label Mar 17, 2022
@the-knights-who-say-ni the-knights-who-say-ni added the CLA signed label Mar 17, 2022
This was referenced Mar 17, 2022
bpo-46841: Quicken code in-place #31888
Merged
Inline caching faster-cpython/ideas#310
Closed
@markshannon

markshannon commented Mar 18, 2022

Copy link
Copy Markdown
Member

I'm not sure that this is worthwhile, at least not in this form.

The current approach is very fragile, as you rightly point out. However, this PR moves code from a cold path generator.throw() onto the hot path of a yield from.

Although the points you raise are valid, they all safe for now (although they probably won't be in future).

It doesn't properly handle the case where SEND has an EXTENDED_ARG.

Until the compiler freely re-orders blocks, the oparg will only ever be very small as it only jumps over a couple of instructions.

It scans backwards, which is susceptible to accidentally reading inline cache entries as if they were valid instructions.

Again, it's OK because we know what instructions we are scanning over.

It jumps from a YIELD_VALUE instruction, which isn't really supposed to be possible.

Technically, it steps back to the SEND and then jumps.


IMO, the "proper" fix for this is to wrap the YIELD_VALUE after the SEND in a try-except that throws the exception into the inner generator, as the only way a YIELD_VALUE can raise is in a throw().

Now:

start:
    SEND end
    YIELD_VALUE
    RESUME
    JUMP_NO_INTERRUPT start
handler:
    THROW_INTO # Maybe a call to an intrinsic function: CALL_INTRINSIC "throw_into", 1
end:

With hidden try-except:

start:
    SEND end
    SETUP_FINALLY handler
    YIELD_VALUE
    POP_BLOCK
    RESUME
    JUMP_NO_INTERRUPT start
handler:
    THROW_INTO # Throws the exception into the sub-generator.
end:

This was part of the motivation for splitting out SEND in the first place.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@brandtbucher

brandtbucher commented Mar 18, 2022

Copy link
Copy Markdown
Member Author

Technically, it steps back to the SEND and then jumps.

This seems unhelpfully pedantic.

IMO, the "proper" fix for this is to wrap the YIELD_VALUE after the SEND in a try-except that throws the exception into the inner generator, as the only way a YIELD_VALUE can raise is in a throw().

I don’t think I agree (at least not yet). It's not too clear how your suggestion would work, so just to make sure my understanding is correct:

  • _gen_throw will set the current exception to whatever we're throwing, then call PyEval_EvalFrameEx(gen_frame, /*throwflag=*/1).
  • This will pop TOS (which is the last thing we yielded, and we don't care about anymore) and jump to THROW_INTO, which performs the equivalent of unsetting the current exception and calling TOS.throw(exc_type, exc_value, exc_traceback).
  • The above steps keep happening all the way down the chain.
  • When one of the calls returns, THROW_INTO will handle three separate cases:
    • The throw() call raised an exception:
      • It's a StopIteration, so SET_TOP(return_value) and start the next instruction?
      • It's something else, so goto error;.
    • It yielded the next value, so... push the new value, and jump back into the yield from/await loop?

I'm not sure if I got that right, since a lot of this code is new to me. If so, it seems much, much more complex and expensive than what I've proposed here: clearing the sub-iterator when it returns and adding a single, easily-predictable NULL check to SEND.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@markshannon

markshannon commented Mar 20, 2022 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

I hadn't considered the special case for StopIteration. THROW_INTO shouldn't have to do any more work than SEND.

If THROW_INTO were the same as SEND, except that it calls TOS.throw() instead of TOS.send(), then the following should work:

start:
    SEND end
    SETUP_FINALLY handler
    YIELD_VALUE
    POP_BLOCK
    RESUME
    JUMP_NO_INTERRUPT start
handler:
    THROW_INTO end
    RESUME
    JUMP_NO_INTERRUPT start
end:
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@markshannon

markshannon commented Mar 20, 2022

Copy link
Copy Markdown
Member

Technically, it steps back to the SEND and then jumps.

This seems unhelpfully pedantic.

No, this is an important detail.
Because it means that we know the step backwards is correct, for the reasons already given, and that we are jumping from an instruction that is a jump, so that is also OK.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@brandtbucher
Catch up with main
4d30025
@brandtbucher brandtbucher mentioned this pull request Apr 10, 2022
Inline bytecode caches #90997
Closed
@brandtbucher
Catch up with main
f3d7fbe
@brandtbucher

brandtbucher commented May 6, 2022 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member Author

With the beta freeze looming, I strongly feel that we should go with this approach (at least for 3.11). It's a working, very minimal change to the existing code that helps other projects (like JITs, PEP 523 hooks, etc.) that need this logic to be moved back into the bytecode. The additional branch is cheap, easily predictable, and fits naturally with the existing code.

For reference, I've now tried two other approaches with virtual try/excepts. Both are considerably more complicated, and add a lot of additional instructions and logic to each yield from/await:

Attempt #1 uses a new THROW opcode in a virtual try/except. It still fails the test suite with the following error that I've spent days trying to debug:

======================================================================
ERROR: test_anext_iter (test.test_asyncgen.AsyncGenAsyncioTest.test_anext_iter) [pure-Python anext()]
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/brandtbucher/cpython/Lib/test/test_asyncgen.py", line 735, in run_test
    test(py_anext)
    ^^^^^^^^^^^^^^
  File "/home/brandtbucher/cpython/Lib/test/test_asyncgen.py", line 674, in test3
    g.close()
    ^^^^^^^^^
  File "/home/brandtbucher/cpython/Lib/test/test_asyncgen.py", line 78, in anext_impl
    return await __anext__(iterator)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: cannot reuse already awaited __anext__()/asend()

----------------------------------------------------------------------

I'm also not confident that the rest of the logic is 100% correct.

Attempt #2 uses a more complicated virtual try/except that catches StopIteration and extracts its value member. It does pass all tests, though.

I'll leave the decision up to you, @markshannon.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@markshannon

markshannon commented May 9, 2022 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Well, this didn't make it into 3.11.
For the best, IMO. The current implementation might be clunky, but it does work. Best not to rush in last minute changes.

Approach 2 passes the tests, and has fewer changes to the instruction set. That seems better than approach 1.
@brandtbucher Which do you prefer?

One other question.
How much of https://peps.python.org/pep-0380/#formal-semantics should we move into the compiler, and how much belongs in helper C code?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

brandtbucher added 2 commits June 8, 2022 09:59
@brandtbucher
Catch up with main
67a63b2
@brandtbucher
Minor doc edits
5aebc87
@brandtbucher brandtbucher mentioned this pull request Jun 21, 2022
Add more tests for throwing into yield from #94097
Merged
@brandtbucher brandtbucher self-assigned this Jun 27, 2022
@ezio-melotti ezio-melotti removed the CLA signed label Jul 13, 2022
@brandtbucher brandtbucher mentioned this pull request Aug 15, 2022
GH-90997: Wrap yield from/await in a virtual try/except StopIteration #96010
Merged
@brandtbucher

brandtbucher commented Aug 15, 2022

Copy link
Copy Markdown
Member Author

Closing in favor of #96010.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@brandtbucher brandtbucher closed this Aug 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@markshannon markshannon Awaiting requested review from markshannon markshannon is a code owner
@1st1 1st1 Awaiting requested review from 1st1

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@brandtbucher brandtbucher

Labels

awaiting core review

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

5 participants

@brandtbucher @markshannon @the-knights-who-say-ni @ezio-melotti @bedevere-bot
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.