Python 3.13: CALL self-slot fix, CALL_KW, and assorted missing opcodes#601
Closed
sfz001 wants to merge 1 commit into
Closed
Python 3.13: CALL self-slot fix, CALL_KW, and assorted missing opcodes#601sfz001 wants to merge 1 commit into
sfz001 wants to merge 1 commit into
Conversation
Python 3.13 introduced several opcodes that pycdc did not handle,
and it also changed the relative position of the NULL self-slot
that surrounds the callable around ``CALL``. When the callable is
loaded via ``LOAD_GLOBAL + NULL`` the layout is
[NULL, callable, args...]
but when it is loaded via ``LOAD_ATTR`` followed by a separate
``PUSH_NULL`` (the common pattern for non-method calls like
``mod.func(...)`` starting in 3.13) the layout is
[callable, NULL, args...]
The existing CALL_A handler only recognised the first layout, so
calls of the second form decompiled to ``None(...)``. Check for
both arrangements.
New opcodes handled:
* ``MAKE_FUNCTION`` (no-operand 3.13 form) and
``SET_FUNCTION_ATTRIBUTE`` -- push the code object as an empty
``ASTFunction`` and keep it on TOS while attribute values are
attached (defaults/kwdefaults/annotations/closure cells are
discarded for now; see #579/#581 for fuller support).
* ``CALL_KW`` -- replaces the KW_NAMES + CALL pattern; kw-names
tuple sits at TOS, the operand is the total argument count.
* ``TO_BOOL`` -- no-op for decompilation purposes (fixes #540).
* ``MAKE_CELL``, ``COPY_FREE_VARS`` -- prologue ops, no stack effect.
* ``LOAD_FAST_CHECK``, ``LOAD_FAST_AND_CLEAR`` -- behave like LOAD_FAST.
* ``STORE_FAST_STORE_FAST``, ``STORE_FAST_LOAD_FAST`` -- 3.13 fused
locals ops; split into their component operations.
* ``POP_JUMP_IF_NONE``, ``POP_JUMP_IF_NOT_NONE`` -- reuse the
existing POP_JUMP_IF_FALSE/TRUE pipeline.
Fixes #540, #587 and part of the missing-opcode list called out
in #547.
Adds ``call_kw_3_13`` test that exercises both the ``CALL_KW``
path and ``LOAD_ATTR + PUSH_NULL + CALL_KW`` (``json.dumps({},
ensure_ascii=False)``), which previously rendered as ``None(...)``.
zrax
reviewed
Apr 22, 2026
Comment on lines
+1731
to
+1733
| // operand). Keep the function on top; discard the | ||
| // attribute value for now since pycdc does not yet | ||
| // render these extras. |
Owner
There was a problem hiding this comment.
Suggestion: Might be worth adding an xfail case for these attributes if possible, so it's easier to see what's missing...
| break; | ||
| case Pyc::LOAD_FAST_CHECK_A: | ||
| // Python 3.12+: like LOAD_FAST but raises if unbound. For | ||
| // decompilation purposes they are interchangeable. |
Owner
There was a problem hiding this comment.
Probably worth merging these cases rather than duplicating their content. The comments can be added under the case labels, with a fallthrough attribute if the compiler requires it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Adds the Python 3.13 call-path opcodes that pycdc was still missing, and fixes a latent bug in
CALL_Awhere the direction of theNULLself-slot surrounding the callable was hard-coded.The call self-slot direction bug
In 3.11+, every
CALLexpects both a callable and aNULLself-slot on the stack. Which one is deeper depends on how the callable was loaded:LOAD_GLOBALwith the+NULLflag pushes[NULL, callable](NULL deeper);LOAD_ATTRfollowed by a separatePUSH_NULLpushes[callable, NULL](NULL on top).The second form is the common pattern for non-method calls in 3.13 (e.g.
json.dumps({}, ensure_ascii=False)). The existing handler only looked for the first layout, so those calls decompiled asNone(...). We now check for both arrangements.New opcodes handled
MAKE_FUNCTION(no operand, 3.13)ASTFunctionSET_FUNCTION_ATTRIBUTECALL_KWTO_BOOLMAKE_CELL,COPY_FREE_VARSLOAD_FAST_CHECK,LOAD_FAST_AND_CLEARLOAD_FASTSTORE_FAST_STORE_FAST,STORE_FAST_LOAD_FASTPOP_JUMP_IF_NONE,POP_JUMP_IF_NOT_NONEPOP_JUMP_IF_FALSE/TRUEpipelineThe
SET_FUNCTION_ATTRIBUTEhandler currently discards the attribute value (defaults/kwdefaults/annotations/closure cells). That is intentionally minimal — PRs #579 and #581 have more complete treatments. Until one of those lands, this at least lets functions decompile instead of silently crashing.Test
Added
tests/input/call_kw_3_13.py, compiled as.3.13.pyc, covering both theCALL_KWandLOAD_ATTR + PUSH_NULL + CALL_KWpatterns:Before this patch the last line decompiled to
None({}, ensure_ascii=False).What this does not fix
Still missing from 3.13 decompilation (filed in #547 and elsewhere):
FORMAT_SIMPLE/CONVERT_VALUE(f-strings)CALL_INTRINSIC_1/CALL_INTRINSIC_2(most variants)CALL_FUNCTION_EXDICT_MERGE/DICT_UPDATE/MAP_ADDNODE_TYPEPARAM, seen asUnsupported Node type: 27)I have ad-hoc stubs for a few of those in a working branch, but they're hacks; better to leave them for a follow-up once someone implements them properly.