Skip to content

Commit a73a141

Browse files
committed
Add Python 3.14 opcode support and 3.11+ zero-cost exception decompilation
Extend the decompiler to handle Python 3.14 bytecode and reconstruct try/except/finally from the zero-cost exception model introduced in 3.11. Python 3.14: - Update the 3.14 magic number and remap opcodes to their argument-bearing forms (LOAD_FAST_BORROW, LOAD_COMMON_CONSTANT, LOAD_SPECIAL, BUILD_INTERPOLATION, STORE_FAST_MAYBE_NULL). - Decode LOAD_SMALL_INT, LOAD_COMMON_CONSTANT, LOAD_SPECIAL, and the LOAD_FAST_BORROW family; treat LOAD_FAST_CHECK as a plain fast load. - Reconstruct PEP 750 t-strings via BUILD_INTERPOLATION / BUILD_TEMPLATE, reusing the f-string rendering path and prefixing templates with 't'. - Normalize the standalone PUSH_NULL layout so CALL sees NULL, callable. - Skip specialized/adaptive opcodes defensively rather than aborting. Zero-cost exceptions (3.11+): - Pre-scan co_exceptiontable to locate try bodies and handlers, then rebuild try/except/finally structure while walking the linear byte stream. - Recover `except <type>:`, bare `except:`, and `except ... as <name>`, suppressing the compiler-generated alias cleanup. Tests: - Add except_as covering bare, typed, and aliased except clauses under the Python 3.14 zero-cost exception layout.
1 parent 75c2e8d commit a73a141

8 files changed

Lines changed: 690 additions & 27 deletions

File tree

ASTNode.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -578,9 +578,15 @@ class ASTCondBlock : public ASTBlock {
578578
PycRef<ASTNode> cond() const { return m_cond; }
579579
bool negative() const { return m_negative; }
580580

581+
/* For BLK_EXCEPT: the optional `as <name>` alias (Python 3.11+ zero-cost
582+
exception reconstruction). */
583+
PycRef<ASTNode> exceptAs() const { return m_exceptAs; }
584+
void setExceptAs(PycRef<ASTNode> name) { m_exceptAs = std::move(name); }
585+
581586
private:
582587
PycRef<ASTNode> m_cond;
583588
bool m_negative;
589+
PycRef<ASTNode> m_exceptAs;
584590
};
585591

586592

@@ -717,13 +723,16 @@ class ASTJoinedStr : public ASTNode {
717723
public:
718724
typedef std::list<PycRef<ASTNode>> value_t;
719725

720-
ASTJoinedStr(value_t values)
721-
: ASTNode(NODE_JOINEDSTR), m_values(std::move(values)) { }
726+
ASTJoinedStr(value_t values, bool is_template = false)
727+
: ASTNode(NODE_JOINEDSTR), m_values(std::move(values)),
728+
m_template(is_template) { }
722729

723730
const value_t& values() const { return m_values; }
731+
bool isTemplate() const { return m_template; }
724732

725733
private:
726734
value_t m_values;
735+
bool m_template;
727736
};
728737

729738
class ASTAnnotatedVar : public ASTNode {

ASTree.cpp

Lines changed: 609 additions & 12 deletions
Large diffs are not rendered by default.

bytecode_ops.inl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,6 @@ OPCODE(BUILD_TEMPLATE) // Python 3.14 ->
128128
OPCODE(BINARY_OP_INPLACE_ADD_UNICODE)
129129
OPCODE(NOT_TAKEN)
130130
OPCODE(POP_ITER)
131-
OPCODE(BUILD_INTERPOLATION)
132-
OPCODE(LOAD_COMMON_CONSTANT)
133-
OPCODE(LOAD_FAST_BORROW)
134-
OPCODE(LOAD_FAST_BORROW_LOAD_FAST_BORROW)
135-
OPCODE(LOAD_SPECIAL)
136131
OPCODE(BINARY_OP_ADD_FLOAT)
137132
OPCODE(BINARY_OP_ADD_INT)
138133
OPCODE(BINARY_OP_ADD_UNICODE)
@@ -220,7 +215,6 @@ OPCODE(ANNOTATIONS_PLACEHOLDER)
220215
OPCODE(JUMP)
221216
OPCODE(JUMP_NO_INTERRUPT)
222217
OPCODE(SETUP_CLEANUP)
223-
OPCODE(STORE_FAST_MAYBE_NULL)
224218

225219
/* Has parameter word */
226220
OPCODE_A_FIRST(STORE_NAME) // Python 1.0 -> names[A]
@@ -371,6 +365,12 @@ OPCODE_A(SET_FUNCTION_ATTRIBUTE) // Python 3.13 -> A=attrib
371365
OPCODE_A(STORE_FAST_LOAD_FAST) // Python 3.13 -> A=locals[A<<4]+locals[A&0xf]
372366
OPCODE_A(STORE_FAST_STORE_FAST) // Python 3.13 -> A=locals[A<<4]+locals[A&0xf]
373367
OPCODE_A(LOAD_SMALL_INT) // Python 3.14 -> A=small int range(256)
368+
OPCODE_A(LOAD_FAST_BORROW) // Python 3.14 -> locals[A]
369+
OPCODE_A(LOAD_FAST_BORROW_LOAD_FAST_BORROW) // Python 3.14 -> A=locals[A<<4]+locals[A&0xf]
370+
OPCODE_A(LOAD_COMMON_CONSTANT) // Python 3.14 -> A=common_constant_index
371+
OPCODE_A(LOAD_SPECIAL) // Python 3.14 -> A=special_method_index
372+
OPCODE_A(BUILD_INTERPOLATION) // Python 3.14 -> A=(format&0x03)+(conversion<<2)
373+
OPCODE_A(STORE_FAST_MAYBE_NULL) // Python 3.14 -> locals[A]
374374

375375
/* Instrumented opcodes */
376376
OPCODE_A(INSTRUMENTED_LOAD_SUPER_ATTR) // Python 3.12 -> (see LOAD_SUPER_ATTR)

bytes/python_3_14.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ BEGIN_MAP(3, 14)
4646
MAP_OP(42, UNARY_NOT)
4747
MAP_OP(43, WITH_EXCEPT_START)
4848
MAP_OP(44, BINARY_OP_A)
49-
MAP_OP(45, BUILD_INTERPOLATION)
49+
MAP_OP(45, BUILD_INTERPOLATION_A)
5050
MAP_OP(46, BUILD_LIST_A)
5151
MAP_OP(47, BUILD_MAP_A)
5252
MAP_OP(48, BUILD_SET_A)
@@ -82,21 +82,21 @@ BEGIN_MAP(3, 14)
8282
MAP_OP(78, LIST_APPEND)
8383
MAP_OP(79, LIST_EXTEND_A)
8484
MAP_OP(80, LOAD_ATTR_A)
85-
MAP_OP(81, LOAD_COMMON_CONSTANT)
85+
MAP_OP(81, LOAD_COMMON_CONSTANT_A)
8686
MAP_OP(82, LOAD_CONST_A)
8787
MAP_OP(83, LOAD_DEREF_A)
8888
MAP_OP(84, LOAD_FAST_A)
8989
MAP_OP(85, LOAD_FAST_AND_CLEAR_A)
90-
MAP_OP(86, LOAD_FAST_BORROW)
91-
MAP_OP(87, LOAD_FAST_BORROW_LOAD_FAST_BORROW)
90+
MAP_OP(86, LOAD_FAST_BORROW_A)
91+
MAP_OP(87, LOAD_FAST_BORROW_LOAD_FAST_BORROW_A)
9292
MAP_OP(88, LOAD_FAST_CHECK_A)
9393
MAP_OP(89, LOAD_FAST_LOAD_FAST_A)
9494
MAP_OP(90, LOAD_FROM_DICT_OR_DEREF_A)
9595
MAP_OP(91, LOAD_FROM_DICT_OR_GLOBALS_A)
9696
MAP_OP(92, LOAD_GLOBAL_A)
9797
MAP_OP(93, LOAD_NAME_A)
9898
MAP_OP(94, LOAD_SMALL_INT_A)
99-
MAP_OP(95, LOAD_SPECIAL)
99+
MAP_OP(95, LOAD_SPECIAL_A)
100100
MAP_OP(96, LOAD_SUPER_ATTR_A)
101101
MAP_OP(97, MAKE_CELL_A)
102102
MAP_OP(98, MAP_ADD_A)
@@ -238,5 +238,5 @@ BEGIN_MAP(3, 14)
238238
MAP_OP(263, SETUP_CLEANUP)
239239
MAP_OP(264, SETUP_FINALLY_A)
240240
MAP_OP(265, SETUP_WITH_A)
241-
MAP_OP(266, STORE_FAST_MAYBE_NULL)
241+
MAP_OP(266, STORE_FAST_MAYBE_NULL_A)
242242
END_MAP()

pyc_module.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ enum PycMagic {
3636
MAGIC_3_11 = 0x0A0D0DA7,
3737
MAGIC_3_12 = 0x0A0D0DCB,
3838
MAGIC_3_13 = 0x0A0D0DF3,
39-
MAGIC_3_14 = 0x0A0D0E29,
39+
MAGIC_3_14 = 0x0A0D0E2B,
4040

4141
INVALID = 0,
4242
};

tests/compiled/except_as.3.14.pyc

763 Bytes
Binary file not shown.

tests/input/except_as.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
def bare():
2+
try:
3+
a = 1
4+
except:
5+
a = 2
6+
b = 3
7+
8+
9+
def typed():
10+
try:
11+
a = 1
12+
except ValueError:
13+
a = 2
14+
b = 3
15+
16+
17+
def alias():
18+
try:
19+
a = 1
20+
except ValueError as e:
21+
a = e
22+
b = 3

tests/tokenized/except_as.txt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
def bare ( ) : <EOL>
2+
<INDENT>
3+
try : <EOL>
4+
<INDENT>
5+
a = 1 <EOL>
6+
<OUTDENT>
7+
except : <EOL>
8+
<INDENT>
9+
a = 2 <EOL>
10+
<OUTDENT>
11+
b = 3 <EOL>
12+
<OUTDENT>
13+
def typed ( ) : <EOL>
14+
<INDENT>
15+
try : <EOL>
16+
<INDENT>
17+
a = 1 <EOL>
18+
<OUTDENT>
19+
except ValueError : <EOL>
20+
<INDENT>
21+
a = 2 <EOL>
22+
<OUTDENT>
23+
b = 3 <EOL>
24+
<OUTDENT>
25+
def alias ( ) : <EOL>
26+
<INDENT>
27+
try : <EOL>
28+
<INDENT>
29+
a = 1 <EOL>
30+
<OUTDENT>
31+
except ValueError as e : <EOL>
32+
<INDENT>
33+
a = e <EOL>
34+
<OUTDENT>
35+
b = 3 <EOL>

0 commit comments

Comments
 (0)