Skip to content
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

[PEP 558 - WIP] bpo-30744: Trace hooks no longer reset closure state #3640

Open
wants to merge 44 commits into
base: master
from

Conversation

@ncoghlan
Copy link
Contributor

@ncoghlan ncoghlan commented Sep 18, 2017

Previously, trace hooks running on a nested function
could incorrectly reset the state of a closure cell.

This avoids that by changing the semantics of the
f_locals namespace on function frames to use a
write-through proxy, such that changes are made
immediately rather than being written back some
arbitrary time later.

PEP 558 and bpo-17960 cover additional changes
to locals() and frame.f_locals semantics that are
part of this update.

Remaining TODO items before this PR could be considered complete:

  • Python API documentation updates for locals(), vars(), and potentially others (check all locals mentions)
  • docstring updates for locals() (and potentially vars())
  • PyEval_* C API documentation updates
  • PyFrame_* C API documentation updates
  • Fix C API header layout to follow modern conventions (#18052)
  • When rearranging headers, fix the clang warning/error about PyFrameObject redefinition
  • Add refcount adjustment info for new C APIs
  • Migrate eval() default locals namespace to PyLocals_Get()
  • Migrate exec() default locals namespace to PyLocals_Get()
  • Report an error if IMPORT_STAR opcode is encountered in a CO_OPTIMIZED frame
  • Add an explicit test for the new PyFrame_LocalsToFast() runtime error
  • Add explicit C API tests for PyLocals_Get() and PyEval_GetLocals() at different scopes
  • Implement and test the other new PyLocals and PyFrame functions
  • Implement and test the following methods on the fast locals proxy:
    • setdefault()
    • popitem()
    • clear()
    • update() (theoretically already implemented, but needs explicit tests)

Desirable code structure improvements (may be best handled as separate refactoring PRs rather than including them directly in this PR):

  • Deduplicate the mutable mapping code copied from odictobject.c
  • Move the DictProxy code out of descrobject.c

https://bugs.python.org/issue30744

@ncoghlan ncoghlan changed the title [DO NOT MERGE - PEP 558] bpo-30744: Trace hooks no longer reset closure state [PEP 558 - DO NOT MERGE] bpo-30744: Trace hooks no longer reset closure state Sep 18, 2017
Previously, trace hooks running on a nested function
could incorrectly reset the state of a closure cell.

This avoids that by slightly changing the semantics
of the locals namespace in functions, generators,
and coroutines, such that closure references are
represented in the locals namespace by their
actual cell objects while a trace hook implemented
in Python is running.

Depends on PEP 558 and bpo-17960 to cover the
underlying change to frame.f_locals semantics.
@ncoghlan ncoghlan force-pushed the ncoghlan:bpo-30744-make-locals-closure-safe branch from 05a351e to 7626a0e Nov 5, 2017
Objects/frameobject.c Outdated Show resolved Hide resolved
Copy link

@auvipy auvipy left a comment

rebase needed and build failures needed addressing.

@ncoghlan
Copy link
Contributor Author

@ncoghlan ncoghlan commented May 22, 2019

@njsmith Aside from the TODO's in the PR itself, this is a pretty complete implementation of the current version of PEP 558 now.

@ncoghlan ncoghlan requested a review from njsmith May 22, 2019
@ncoghlan
Copy link
Contributor Author

@ncoghlan ncoghlan commented May 22, 2019

One other known issue: the header file updates still need to be modified to conform with the new conventions.

@scotchka
Copy link
Contributor

@scotchka scotchka commented May 23, 2019

I don't know if this is already addressed elsewhere, but I'm getting a strange error. Running the below script with python.exe built with this branch (I removed an unused parameter to make it compile):

def foo():
    import pdb; pdb.set_trace()
    x=1

foo()

gives

$ ./python.exe jump.py
> /Users/henry/pep558/cpython/jump.py(3)foo()
-> x=1
(Pdb) x=123
(Pdb) c
python.exe(69485,0x10961d5c0) malloc: *** error for object 0x1029ee790: pointer being freed was not allocated
python.exe(69485,0x10961d5c0) malloc: *** set a breakpoint in malloc_error_break to debug
Abort trap: 6

but this error is absent for certain values of x, say:

$ ./python.exe jump.py
> /Users/henry/pep558/cpython/jump.py(3)foo()
-> x=1
(Pdb) x=1234
(Pdb) c
$
@ncoghlan
Copy link
Contributor Author

@ncoghlan ncoghlan commented May 25, 2019

Hmm, looks like I may have a refcounting bug, and the small integer cache is able to provoke it into crashing.

@scotchka
Copy link
Contributor

@scotchka scotchka commented May 26, 2019

I can report that not all small numbers trigger this error. On my machine x = 11, say, is fine. Most perplexing!

@ncoghlan
Copy link
Contributor Author

@ncoghlan ncoghlan commented May 27, 2019

Running test_frame consistently triggers a crash in test_clear_locals for me, so I'm focusing on that initially.

@ncoghlan
Copy link
Contributor Author

@ncoghlan ncoghlan commented May 27, 2019

Last push resolves the test_frame segfault, but adds a TODO noting that the locals proxy may still misbehave after a frame has been cleared (or is in the process of being destroyed).

There's also a threading test which checks reference cycles aren't being created through the frames, which is currently failing (quite possibly for a related reason)

@ncoghlan
Copy link
Contributor Author

@ncoghlan ncoghlan commented May 30, 2019

Ugh, the refcyle issue detected by test_threading is kinda inherent in the current implementation: once frame.f_locals gets created, there's now a cyclic reference between the frame and its own locals proxy.

I'm going to try forcibly breaking the cycle when an optimized frame exits (before the eval loop drops its own reference).

@ncoghlan
Copy link
Contributor Author

@ncoghlan ncoghlan commented Dec 29, 2019

ncoghlan@7078632 fixed the test_sys_settrace crash (mismanaged refcounts when local variables were updated via the write-through proxy).

There's still another segfault in test_log_destroyed_pending_task in test_asyncio, though.

@ncoghlan
Copy link
Contributor Author

@ncoghlan ncoghlan commented Dec 29, 2019

It appears test_asyncio was hitting the same bug as test_frame (the pop/delete support in the write-through proxy wasn't quite right), as they're both passing for me after the latest change.

@ncoghlan ncoghlan changed the title [PEP 558 - DO NOT MERGE] bpo-30744: Trace hooks no longer reset closure state [PEP 558 - WIP] bpo-30744: Trace hooks no longer reset closure state Dec 30, 2019
@ncoghlan
Copy link
Contributor Author

@ncoghlan ncoghlan commented Dec 30, 2019

This PR is now a solid proof of concept for the PEP, but there's a lot of cleanup to be done before it would be ready for merging (and that's assuming the PEP is accepted in its current form - the latest draft is still to be discussed on python-dev).

I've made notes in the PR description of all the missing pieces that I'm already aware of (including the Mac OS X build failure, which is a clang vs gcc issue).

@ncoghlan
Copy link
Contributor Author

@ncoghlan ncoghlan commented Jan 18, 2020

Latest round of PEP review resulted in several changes to the proposed C API, including a clearer separation between additions to the stable ABI and additions to the full CPython C API. I created #18052 as a preparatory refactoring that migrates frameobject.h to the modern header layout that clearly separates the two APIs.

@ncoghlan
Copy link
Contributor Author

@ncoghlan ncoghlan commented Feb 2, 2020

I made enough progress on migrating to the redesigned API described in https://discuss.python.org/t/pep-558-defined-semantics-for-locals/2936/11?u=ncoghlan that next weekend I'll update the PEP to cover this version of the API (I didn't want to do that until I'd made the initial draft updates here).

@codecov
Copy link

@codecov codecov bot commented Feb 2, 2020

Codecov Report

Merging #3640 into master will not change coverage by %.
The diff coverage is n/a.

Impacted file tree graph

@@           Coverage Diff           @@
##           master    #3640   +/-   ##
=======================================
  Coverage   82.03%   82.03%           
=======================================
  Files        1956     1956           
  Lines      584511   584511           
  Branches    44467    44467           
=======================================
  Hits       479520   479520           
  Misses      95364    95364           
  Partials     9627     9627           

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 161ad47...161ad47. Read the comment docs.

ncoghlan added 7 commits Feb 15, 2020
Mark Shannon pointed out that if every access to the Python
level frame f_locals attributes creates a new write-through
proxy instance, there's no need to actually store the proxy
itself on the frame.

Instead, we can continue to store only the shared mapping
returned by PyEval_GetLocals(), and avoid creating a
circular reference that we then need to break when frame
evaluation ends.
@ncoghlan
Copy link
Contributor Author

@ncoghlan ncoghlan commented Feb 25, 2020

Thanks to a suggestion by @markshannon, I was able to get rid of the circular reference between the frame and the fast locals proxy objects (the frame has gone back to holding only the dynamic snapshot that it stores now, and the proxies are created on demand as part of the "frame.f_locals" get descriptor)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

7 participants
You can’t perform that action at this time.