From 03aab08636825c7d977524281855a6250ff8bc8d Mon Sep 17 00:00:00 2001 From: Ben Carr Date: Sun, 31 Aug 2025 22:00:26 +0000 Subject: [PATCH 1/8] Add failing test for Python 3.13 Make Function & Set Function Attribute --- tests/compiled/test_functions_py3.3.13.pyc | Bin 0 -> 2476 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/compiled/test_functions_py3.3.13.pyc diff --git a/tests/compiled/test_functions_py3.3.13.pyc b/tests/compiled/test_functions_py3.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8efd90e34eb2591fbd4e5c8a9b2c7cbc25d9e7c2 GIT binary patch literal 2476 zcmb_d$xa(V5barP%wjgP8c={Niiu)k)+iz554iLPpz#<83Bjla6v!z*ft1{G^*KKi zS-IuJ&B9r(c~xW2j13-ga7$fNQ(g6H>F(-!I^Cx*YA?UNS+bP+$v@3uH<$D3M1KTY8`J}$vDPqoLS##WQL+n)C3Gng4xlCtvtb6LC4B_z(gdWk9MtuSbH3s_xx6dQ5myPjYVDWH zRjcR_$dMxsJ>IT)41W`?^EHR&AmTV8rD^xksWdia*>Hx*EECD+hL0lM!y;!DB@>>n=Tp;bJQ;WAV5n&Wafpy6s4AIP?Sn4>2S0 z{wFRi!K@vyNKYZnNsp1XAlF##oLmq& z)N&Bz@W>v^wn!x;?QPR*#KSpzUqEV>VHnq@Vf0=nJCI+z p5E9X=CDmx=x57fmH5VqQWI-%yZ2NiP4{P4F-q8J8_{-W7`ak)LM^OL( literal 0 HcmV?d00001 From d45b3d76899c4dacee01583e457ca4b308a8b88f Mon Sep 17 00:00:00 2001 From: Ben Carr Date: Sun, 31 Aug 2025 22:13:36 +0000 Subject: [PATCH 2/8] Changes to LOAD_ATTR and CALL for Python 3.13 arg ordering on the stack --- ASTree.cpp | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/ASTree.cpp b/ASTree.cpp index 0a10848f0..65648f4c4 100644 --- a/ASTree.cpp +++ b/ASTree.cpp @@ -453,8 +453,15 @@ PycRef BuildFromCode(PycRef code, PycModule* mod) stack.pop(); PycRef function = stack.top(); stack.pop(); + if(mod->verCompare(3, 13) >= 0){ + /* As of Python 3.13 CALL now has self or NULL above the + callable, but below the positional args in the stack*/ + PycRef self = stack.top(); // Self or null + stack.pop(); + } PycRef loadbuild = stack.top(); stack.pop(); + int loadbuild_type = loadbuild.type(); if (loadbuild_type == ASTNode::NODE_LOADBUILDCLASS) { PycRef call = new ASTCall(function, pparamList, kwparamList); @@ -514,11 +521,23 @@ PycRef BuildFromCode(PycRef code, PycModule* mod) pparamList.push_front(param); } } - PycRef func = stack.top(); - stack.pop(); - if ((opcode == Pyc::CALL_A || opcode == Pyc::INSTRUMENTED_CALL_A) && - stack.top() == nullptr) { + + PycRef func; + PycRef self; + if (mod->verCompare(3, 13) >= 0) { + /* Changed in 3.13: + self or NULL always appears in the same place, above the function, below pos args */ + self = stack.top(); // May be NULL + stack.pop(); + func = stack.top(); + stack.pop(); + } else { + func = stack.top(); stack.pop(); + if ((opcode == Pyc::CALL_A || opcode == Pyc::INSTRUMENTED_CALL_A) && + stack.top() == nullptr) { + stack.pop(); + } } stack.push(new ASTCall(func, pparamList, kwparamList)); @@ -1479,7 +1498,7 @@ PycRef BuildFromCode(PycRef code, PycModule* mod) if (name.type() != ASTNode::NODE_IMPORT) { stack.pop(); - if (mod->verCompare(3, 12) >= 0) { + if (mod->verCompare(3, 12) == 0) { if (operand & 1) { /* Changed in version 3.12: If the low bit of name is set, then a NULL or self is pushed to the stack @@ -1487,6 +1506,22 @@ PycRef BuildFromCode(PycRef code, PycModule* mod) stack.push(nullptr); } operand >>= 1; + stack.push(new ASTBinary(name, new ASTName(code->getName(operand)), ASTBinary::BIN_ATTR)); + break; + } + + if (mod->verCompare(3, 13) >= 0) { + if (operand & 0x01) { + /* Changed AGAIN in 3.13: + Not currently in the docs, but the source confirms + this has changed to match the callable below Self/NULL + rules for the CALL Opcode */ + stack.pop(); + operand >>= 1; + stack.push(new ASTBinary(name, new ASTName(code->getName(operand)), ASTBinary::BIN_ATTR)); + stack.push(nullptr); + } + break; } stack.push(new ASTBinary(name, new ASTName(code->getName(operand)), ASTBinary::BIN_ATTR)); From ed59697ddde169727b5a49db23e9c2b8f2bcf5d1 Mon Sep 17 00:00:00 2001 From: Ben Carr Date: Sun, 31 Aug 2025 22:20:05 +0000 Subject: [PATCH 3/8] Implement MAKE_FUNCTION & SET_FUNCTION_ATTRIBUTE_A for Python 3.13 --- ASTree.cpp | 58 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/ASTree.cpp b/ASTree.cpp index 65648f4c4..9bff1d7cc 100644 --- a/ASTree.cpp +++ b/ASTree.cpp @@ -1595,19 +1595,19 @@ PycRef BuildFromCode(PycRef code, PycModule* mod) stack.push(new ASTName(code->getName(operand))); break; case Pyc::MAKE_CLOSURE_A: + case Pyc::MAKE_FUNCTION: case Pyc::MAKE_FUNCTION_A: { PycRef fun_code = stack.top(); stack.pop(); - /* Test for the qualified name of the function (at TOS) */ + /* Test for the qualified name of the function (at TOS), removed in 3.11 */ int tos_type = fun_code.cast()->object().type(); if (tos_type != PycObject::TYPE_CODE && tos_type != PycObject::TYPE_CODE2) { fun_code = stack.top(); stack.pop(); } - ASTFunction::defarg_t defArgs, kwDefArgs; const int defCount = operand & 0xFF; const int kwDefCount = (operand >> 8) & 0xFF; @@ -1643,8 +1643,8 @@ PycRef BuildFromCode(PycRef code, PycModule* mod) defArgs.push_front(stack.top()); stack.pop(); } - } else { - /* From Py 3.6 the operand stopped being an argument count + } else if (mod->verCompare(3, 12) <= 0) { + /* From Py 3.6-12 the operand stopped being an argument count and changed to a flag that indicates what is represented by preceding tuples on the stack. Docs for 3.7 are clearer, docs for 3.6 may have not been correctly updated */ @@ -1672,10 +1672,58 @@ PycRef BuildFromCode(PycRef code, PycModule* mod) defArgs.push_back(new ASTObject(pos)); } } - } + } /* From 3.13 the flag is not longer set on the MAKE_FUNCTION opcode, + but instead on a SET_FUNCTION_ARGUMENT opcode that follows it */ + stack.push(new ASTFunction(fun_code, defArgs, kwDefArgs)); } break; + case Pyc::SET_FUNCTION_ATTRIBUTE_A: + { + PycRef func = stack.top(); + stack.pop(); + if(func.type() != ASTNode::NODE_FUNCTION) { + fputs("Trying to process SET_FUNCTION_ATTRIBUTE when ToS isn't an ASTFunction. This is bad.\n", stderr); + break; + } + PycRef function = func.cast(); + + /* dis Docs suggest the operand is a flag that would allow for multiple attributes + but as mentioned in bytecode.cpp this opcode seems to be a single lookup + for positional + kwargs, so take whatever was previously set and add to it*/ + ASTFunction::defarg_t defArgs = function ->defargs(); + ASTFunction::defarg_t kwDefArgs = function->kwdefargs(); + + /* Argument processing the same as 3.6-12, just incrementally adding + to whatever is already there */ + if(operand & 0x08) { // Cells for free vars to create a closure + stack.pop(); // Ignore these for syntax generation + } + if(operand & 0x04) { // Annotation dict (3.6-9) or string (3.10+) + stack.pop(); // Ignore annotations + } + if(operand & 0x02) { // Kwarg Defaults + PycRef kw_tuple = stack.top(); + stack.pop(); + std::vector> kw_values = kw_tuple.cast()->values(); + + for(const PycRef& kw : kw_values) { + kwDefArgs.push_front(kw); + } + } + if(operand & 0x01) { // Positional Defaults (including positional-or-KW args) + PycRef pos_tuple = stack.top(); + stack.pop(); + std::vector> pos_values = pos_tuple.cast()->object().cast()->values(); + + for(const PycRef& pos : pos_values) { + defArgs.push_back(new ASTObject(pos)); + } + } + + stack.push(new ASTFunction(function->code(), defArgs, kwDefArgs)); + } + break; case Pyc::NOP: break; case Pyc::POP_BLOCK: From 1405891cc8adeb3185c09aba6a891fb72dbeca2c Mon Sep 17 00:00:00 2001 From: Ben Carr Date: Sun, 31 Aug 2025 22:20:54 +0000 Subject: [PATCH 4/8] Filter out extra dunder strings added to functions in 3.13 --- ASTree.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ASTree.cpp b/ASTree.cpp index 9bff1d7cc..3a009cc3f 100644 --- a/ASTree.cpp +++ b/ASTree.cpp @@ -3501,6 +3501,17 @@ void print_src(PycRef node, PycModule* mod, std::ostream& pyc_output) && src.cast()->is_inplace()) { print_src(src, mod, pyc_output); } else { + if(dest.type() == ASTNode::NODE_NAME) { + if (dest.cast()->name()->isEqual("__firstlineno__")) { + // __firstlineno__ = "Records the first line number of a class definition" - Not sure if this is something to do with docstrings + // Automatically added by Python 3.13 and later + break; + } else if (dest.cast()->name()->isEqual("__static_attributes__")) { + // __static_attributes__ = "stores the names of attributes accessed through self.X in any function in a class body" + // Automatically added by Python 3.13 and later + break; + } + } print_src(dest, mod, pyc_output); pyc_output << " = "; print_src(src, mod, pyc_output); From b1081a14e205cfb4523b404b747a2cc440808689 Mon Sep 17 00:00:00 2001 From: Ben Carr Date: Tue, 2 Sep 2025 20:21:00 +0000 Subject: [PATCH 5/8] Tweak documentation on LOAD_ATTR changes --- ASTree.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ASTree.cpp b/ASTree.cpp index 3a009cc3f..9596e1b7d 100644 --- a/ASTree.cpp +++ b/ASTree.cpp @@ -1513,9 +1513,9 @@ PycRef BuildFromCode(PycRef code, PycModule* mod) if (mod->verCompare(3, 13) >= 0) { if (operand & 0x01) { /* Changed AGAIN in 3.13: - Not currently in the docs, but the source confirms - this has changed to match the callable below Self/NULL - rules for the CALL Opcode */ + Not currently in the docs, but the source confirms this has changed to + match the new stacker order rules for the CALL Opcode, where the + Method or Attr is pushed to the stack BEFORE Self or NULL */ stack.pop(); operand >>= 1; stack.push(new ASTBinary(name, new ASTName(code->getName(operand)), ASTBinary::BIN_ATTR)); From 546f5c3470ca70caa9049c0e53e507c7e7f431de Mon Sep 17 00:00:00 2001 From: Ben Carr Date: Tue, 2 Sep 2025 20:40:01 +0000 Subject: [PATCH 6/8] Add additional 3.13 .pyc that failed for LOAD_ATTR due to c.test1() & c.test2 --- tests/compiled/load_method.3.13.pyc | Bin 0 -> 909 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/compiled/load_method.3.13.pyc diff --git a/tests/compiled/load_method.3.13.pyc b/tests/compiled/load_method.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0020d76e69595614b7834ccd74500e63d43c469d GIT binary patch literal 909 zcmZWnO=}Zj5T4o3q$Cvs4boHeAVg@3g;|l-ieCwM z@Dl%nM}L8T!b5q%lc(IYi+I$T{lM7HVfLA?_nCRvv|ir;w$C5GjK1;ry9A}V1xVjB zSi&{90SdN3VaLiV1V^0f6m5h@cv#L9Ir)_J7I58PD-=*!%2rOt&O`2k!|LU(f3J+( zw%1>8z#>R*GFb9Fp#riLlC7+5AV=BkUFER%luQ1|Yx@SfD;eyU)AER5*(0s6Tk@q5 zj-YMz$l>@CBjd!w@v*@m771D{v$YpSgOK*d(_o+{!{o_yu>17HxXCb1ZX3V5n*nJ~ z<@8fHtJN2nGK9W7TAFlZ%nb%hc+}WoB_G;597PWAfIadUw|RU17<_K<#NZ2(MPm`Y zB}O7#PQG7~?@KkVv&+fve>hk?SRhH}l8pZ!583f6N#^XB^NCFBT&mnNBEwKf}CPo{&}Sa412XR}~jRIX@!98#Q&!)O>ywI=y7N#vj=JF}&AoCHbO z*Flm{_;|+3a+j!*lV#l5uHt0J_$s@%aBiWydG4Xx{8eF;cT3OemN?O8^@pv;>Pp&R z{{s017E`&KA90VabALv4hrtgU5r11%-24gRn&5h`x~F&s_|AXnzpfTwZp{zo^c5OK J{HB`i^cO@^rHudp literal 0 HcmV?d00001 From 3fa7e13e06530a92a550ea2f3201404a74078561 Mon Sep 17 00:00:00 2001 From: Ben Carr Date: Tue, 2 Sep 2025 23:04:01 +0000 Subject: [PATCH 7/8] Relocate the removal of __firstlineno__ and __static_attributes__ to a more appropriate area of ASTree --- ASTree.cpp | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/ASTree.cpp b/ASTree.cpp index 9596e1b7d..d0f658fbb 100644 --- a/ASTree.cpp +++ b/ASTree.cpp @@ -3501,17 +3501,6 @@ void print_src(PycRef node, PycModule* mod, std::ostream& pyc_output) && src.cast()->is_inplace()) { print_src(src, mod, pyc_output); } else { - if(dest.type() == ASTNode::NODE_NAME) { - if (dest.cast()->name()->isEqual("__firstlineno__")) { - // __firstlineno__ = "Records the first line number of a class definition" - Not sure if this is something to do with docstrings - // Automatically added by Python 3.13 and later - break; - } else if (dest.cast()->name()->isEqual("__static_attributes__")) { - // __static_attributes__ = "stores the names of attributes accessed through self.X in any function in a class body" - // Automatically added by Python 3.13 and later - break; - } - } print_src(dest, mod, pyc_output); pyc_output << " = "; print_src(src, mod, pyc_output); @@ -3667,6 +3656,18 @@ void decompyle(PycRef code, PycModule* mod, std::ostream& pyc_output) } } } + if (clean->nodes().front().type() == ASTNode::NODE_STORE) { + PycRef store = clean->nodes().front().cast(); + if (store->src().type() == ASTNode::NODE_OBJECT + && store->dest().type() == ASTNode::NODE_NAME) { + PycRef dest = store->dest().cast(); + if (dest->name()->isEqual("__firstlineno__")) { + // __firstlineno__ = "Records the first line number of a class definition" - Not sure if this is something to do with docstrings + // Automatically added by Python 3.13 and later + clean->removeFirst(); + } + } + } // Class and module docstrings may only appear at the beginning of their source if (printClassDocstring && clean->nodes().front().type() == ASTNode::NODE_STORE) { @@ -3688,6 +3689,18 @@ void decompyle(PycRef code, PycModule* mod, std::ostream& pyc_output) clean->removeLast(); // Always an extraneous return statement } } + if (clean->nodes().back().type() == ASTNode::NODE_STORE) { + PycRef store = clean->nodes().back().cast(); + if (store->src().type() == ASTNode::NODE_TUPLE + && store->dest().type() == ASTNode::NODE_NAME) { + PycRef dest = store->dest().cast(); + if (dest->name()->isEqual("__static_attributes__")) { + // __static_attributes__ = "stores the names of attributes accessed through self.X in any function in a class body" + // Automatically added by Python 3.13 and later + clean->removeLast(); + } + } + } } if (printClassDocstring) printClassDocstring = false; From b3d796560049849eeaece118f795895f40a9e738 Mon Sep 17 00:00:00 2001 From: Ben Carr Date: Thu, 4 Sep 2025 21:08:31 +0000 Subject: [PATCH 8/8] Fix LOAD_ATTR to remove extra pops from the 3.13 NULL ordering change. Apply 3.13 NULL ordering change to LOAD_GLOBAL --- ASTree.cpp | 89 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/ASTree.cpp b/ASTree.cpp index d0f658fbb..21a1aee44 100644 --- a/ASTree.cpp +++ b/ASTree.cpp @@ -1495,36 +1495,36 @@ PycRef BuildFromCode(PycRef code, PycModule* mod) case Pyc::LOAD_ATTR_A: { PycRef name = stack.top(); - if (name.type() != ASTNode::NODE_IMPORT) { - stack.pop(); + if (name.type() == ASTNode::NODE_IMPORT) { + break; + } - if (mod->verCompare(3, 12) == 0) { - if (operand & 1) { - /* Changed in version 3.12: - If the low bit of name is set, then a NULL or self is pushed to the stack - before the attribute or unbound method respectively. */ - stack.push(nullptr); - } - operand >>= 1; - stack.push(new ASTBinary(name, new ASTName(code->getName(operand)), ASTBinary::BIN_ATTR)); - break; - } + stack.pop(); + int starting_operand = operand; - if (mod->verCompare(3, 13) >= 0) { - if (operand & 0x01) { - /* Changed AGAIN in 3.13: - Not currently in the docs, but the source confirms this has changed to - match the new stacker order rules for the CALL Opcode, where the - Method or Attr is pushed to the stack BEFORE Self or NULL */ - stack.pop(); - operand >>= 1; - stack.push(new ASTBinary(name, new ASTName(code->getName(operand)), ASTBinary::BIN_ATTR)); - stack.push(nullptr); - } - break; + if (mod->verCompare(3, 12) == 0) { + if (operand & 1) { + /* Changed in version 3.12: + If the low bit of name is set, then a NULL or self is pushed to the stack + before the attribute or unbound method respectively. */ + stack.push(nullptr); } + } + + if (mod->verCompare(3, 12) >= 0) { + operand >>= 1; + } - stack.push(new ASTBinary(name, new ASTName(code->getName(operand)), ASTBinary::BIN_ATTR)); + stack.push(new ASTBinary(name, new ASTName(code->getName(operand)), ASTBinary::BIN_ATTR)); + + if (mod->verCompare(3, 13) >= 0) { + if (starting_operand & 0x01) { + /* Changed AGAIN in 3.13: + Not currently in the docs, but the source confirms this has changed to + match the new stacker order rules for the CALL Opcode, where the + Method or Attr is pushed to the stack BEFORE Self or NULL */ + stack.push(nullptr); + } } } break; @@ -1565,17 +1565,36 @@ PycRef BuildFromCode(PycRef code, PycModule* mod) stack.push(new ASTName(code->getLocal(operand & 0xF))); break; case Pyc::LOAD_GLOBAL_A: - if (mod->verCompare(3, 11) >= 0) { - // Loads the global named co_names[namei>>1] onto the stack. - if (operand & 1) { - /* Changed in version 3.11: - If the low bit of "NAMEI" (operand) is set, - then a NULL is pushed to the stack before the global variable. */ - stack.push(nullptr); + { + int starting_operand = operand; + + if (mod->verCompare(3, 11) == 0 || mod->verCompare(3, 12) == 0 ) { + // Loads the global named co_names[namei>>1] onto the stack. + if (operand & 1) { + /* Changed in version 3.11: + If the low bit of "NAMEI" (operand) is set, + then a NULL is pushed to the stack before the global variable. */ + stack.push(nullptr); + } + } + + if (mod->verCompare(3, 11) >= 0) { + operand >>= 1; + } + + stack.push(new ASTName(code->getName(operand))); + + if (mod->verCompare(3, 13) >= 0) { + // Loads the global named co_names[namei>>1] onto the stack. + if (starting_operand & 0x01) { + /* Changed AGAIN in 3.13: + Not currently in the docs, but the source confirms this has changed to + match the new stacker order rules for the CALL Opcode, where the + Method or Attr is pushed to the stack BEFORE Self or NULL */ + stack.push(nullptr); + } } - operand >>= 1; } - stack.push(new ASTName(code->getName(operand))); break; case Pyc::LOAD_LOCALS: stack.push(new ASTNode(ASTNode::NODE_LOCALS));