Skip to content

Python 3.13: CALL self-slot fix, CALL_KW, and assorted missing opcodes#601

Closed
sfz001 wants to merge 1 commit into
zrax:masterfrom
sfz001:fix-3.13-calls
Closed

Python 3.13: CALL self-slot fix, CALL_KW, and assorted missing opcodes#601
sfz001 wants to merge 1 commit into
zrax:masterfrom
sfz001:fix-3.13-calls

Conversation

@sfz001
Copy link
Copy Markdown

@sfz001 sfz001 commented Apr 22, 2026

Summary

Adds the Python 3.13 call-path opcodes that pycdc was still missing, and fixes a latent bug in CALL_A where the direction of the NULL self-slot surrounding the callable was hard-coded.

The call self-slot direction bug

In 3.11+, every CALL expects both a callable and a NULL self-slot on the stack. Which one is deeper depends on how the callable was loaded:

  • LOAD_GLOBAL with the +NULL flag pushes [NULL, callable] (NULL deeper);
  • LOAD_ATTR followed by a separate PUSH_NULL pushes [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 as None(...). We now check for both arrangements.

New opcodes handled

Opcode Behaviour Related issue
MAKE_FUNCTION (no operand, 3.13) Pop code object, push empty ASTFunction #542, #569, #595
SET_FUNCTION_ATTRIBUTE Keep function on top, drop the attribute value
CALL_KW KW-names tuple at TOS, operand = total arg count #587
TO_BOOL no-op for decompile #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 split into component ops
POP_JUMP_IF_NONE, POP_JUMP_IF_NOT_NONE reuse POP_JUMP_IF_FALSE/TRUE pipeline

The SET_FUNCTION_ATTRIBUTE handler 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 the CALL_KW and LOAD_ATTR + PUSH_NULL + CALL_KW patterns:

import json

def greet(name, greeting='hello'):
    return greeting + ', ' + name

print(greet('world', greeting='hi'))
print(json.dumps({}, ensure_ascii=False))

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_EX
  • DICT_MERGE / DICT_UPDATE / MAP_ADD
  • PEP 695 class-parameter nodes (NODE_TYPEPARAM, seen as Unsupported 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.

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(...)``.
Comment thread ASTree.cpp
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.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggestion: Might be worth adding an xfail case for these attributes if possible, so it's easier to see what's missing...

Comment thread ASTree.cpp
break;
case Pyc::LOAD_FAST_CHECK_A:
// Python 3.12+: like LOAD_FAST but raises if unbound. For
// decompilation purposes they are interchangeable.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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.

@sfz001 sfz001 closed this by deleting the head repository Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants