depyo 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -83
- package/lib/PycDecompiler.js +116 -3
- package/lib/PycReader.js +10 -1
- package/lib/PythonObject.js +7 -1
- package/lib/ast/ast_node.js +39 -2
- package/lib/bytecode/python_3_14.js +1 -1
- package/lib/handlers/binary_ops.js +9 -0
- package/lib/handlers/load_store_names.js +46 -1
- package/lib/handlers/misc_other.js +39 -3
- package/lib/handlers/stack_ops.js +4 -2
- package/lib/handlers/unary_ops.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,116 +1,193 @@
|
|
|
1
1
|
# depyo — Python bytecode decompiler in Node.js
|
|
2
2
|
|
|
3
|
-
Depyo converts Python `.pyc` files (or archives of them) back to readable Python source.
|
|
3
|
+
Depyo converts Python `.pyc` files (or archives of them) back to readable Python source — right from Node.js, without a Python runtime. Coverage spans **Python 1.0 through 3.15** plus PyPy, with first-class support for modern features: match/case, walrus, f-strings, exception groups, async/await, type parameters, PEP 696 TypeVar defaults, and t-strings (PEP 750).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
```bash
|
|
6
|
+
npx depyo my_script.pyc
|
|
7
|
+
# → writes my_script.py next to the input
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## What it's good for
|
|
11
|
+
|
|
12
|
+
- **Reverse engineering stripped Python.** You have a `.pyc` (maybe extracted from a PyInstaller binary, an Android APK's Kivy bundle, or an old archive) and no source. Depyo reconstructs the source — even for Python versions the original `uncompyle6`/`decompyle3` no longer follow.
|
|
13
|
+
- **Malware / threat analysis.** Quickly triage suspicious Python payloads without setting up a matching Python interpreter. Add `--asm` for a bytecode listing alongside the source.
|
|
14
|
+
- **Forensics on old codebases.** Resurrect Python 2.x (even 1.x) modules when the source is long gone.
|
|
15
|
+
- **CI-side audits.** Depyo is a pure Node.js CLI — drop it in any Node pipeline to spot-check compiled `.pyc` against expected sources, or to extract and diff shipped bytecode.
|
|
16
|
+
- **Learning tool.** Inspect how CPython lowers a given Python feature (comprehensions, pattern matching, exception groups) across versions. `--asm` is handy here.
|
|
17
|
+
- **Batch processing.** Feed a `.zip` of `.pyc` files and get back a mirrored tree of `.py` sources.
|
|
18
|
+
|
|
19
|
+
## Why depyo (vs alternatives)
|
|
20
|
+
|
|
21
|
+
| Tool | Versions | Modern features¹ | Runtime | Throughput | Notes |
|
|
22
|
+
| --------------------- | --------------------- | ---------------- | ------- | ---------- | -------------------------------------------- |
|
|
23
|
+
| **depyo** | 1.0–3.15 + PyPy | Yes | Node.js | ~0.1 ms/file² | Modern opcodes land fast; no Python needed |
|
|
24
|
+
| uncompyle6/decompyle3 | 2.x–3.12 (stalled) | Partial | Python | slower | Development largely halted on 3.13+ |
|
|
25
|
+
| pycdc (C++) | 2.x–3.x (limited new) | Partial | native | fast | Rich history, but slow to adopt new opcodes |
|
|
26
|
+
|
|
27
|
+
¹ match/case, walrus, f-strings, exception groups, async/await, type params.
|
|
28
|
+
² Informal: `py314_exception_groups.pyc` × 50 in-process, Node 25, single thread (`--stats` on your machine for real numbers).
|
|
10
29
|
|
|
11
30
|
## Install
|
|
12
|
-
- Global: `npm i -g depyo`
|
|
13
|
-
- One-off: `npx depyo <file.pyc>`
|
|
14
31
|
|
|
15
|
-
|
|
32
|
+
```bash
|
|
33
|
+
npm i -g depyo # global CLI
|
|
34
|
+
npx depyo <file.pyc> # one-off, no install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Node.js 20+ recommended (CI gate).
|
|
16
38
|
|
|
17
39
|
## Quick start
|
|
18
40
|
|
|
19
41
|
```bash
|
|
20
|
-
#
|
|
42
|
+
# Single .pyc → writes <name>.py next to it
|
|
21
43
|
node depyo.js /path/to/file.pyc
|
|
22
44
|
|
|
23
|
-
#
|
|
24
|
-
node depyo.js
|
|
45
|
+
# ZIP of .pyc files → mirrors structure
|
|
46
|
+
node depyo.js my_archive.zip
|
|
25
47
|
|
|
26
|
-
#
|
|
27
|
-
node depyo.js --
|
|
48
|
+
# Also emit disassembly and preserve the raw .pyc
|
|
49
|
+
node depyo.js --asm --raw my_archive.zip
|
|
28
50
|
|
|
29
|
-
#
|
|
51
|
+
# Stream to stdout (no files written)
|
|
30
52
|
node depyo.js --out /path/to/file.pyc
|
|
31
53
|
|
|
32
|
-
#
|
|
33
|
-
node depyo.js --
|
|
34
|
-
node depyo.js --marshal /path/to/blob.bin
|
|
54
|
+
# Flatten outputs (drop mirrored directories)
|
|
55
|
+
node depyo.js --skip-path /path/to/file.pyc
|
|
35
56
|
|
|
36
|
-
#
|
|
37
|
-
node depyo.js --marshal-
|
|
57
|
+
# Headerless marshal blob (no .pyc magic)
|
|
58
|
+
node depyo.js --marshal --py-version 3.11 /path/to/blob.bin
|
|
59
|
+
node depyo.js --marshal /path/to/blob.bin # auto-scan
|
|
60
|
+
node depyo.js --marshal-scan /path/to/blob.bin # fast scan, no decompile
|
|
38
61
|
```
|
|
62
|
+
|
|
39
63
|
Without `--py-version`, depyo scans supported versions (oldest → newest) and accepts the first clean output when all clean candidates agree. If outputs diverge (ambiguous), it stops and asks for `--py-version`. Use `--debug` to see scan results.
|
|
40
64
|
|
|
41
|
-
|
|
42
|
-
- `--asm` emit `.pyasm` disassembly alongside source
|
|
43
|
-
- `--raw` emit raw `.pyc` next to output
|
|
44
|
-
- `--raw-spacing` preserve blank lines/comment gaps
|
|
45
|
-
- `--dump` dump marshalled object tree
|
|
46
|
-
- `--stats` print throughput stats
|
|
47
|
-
- `--skip-source-gen` skip writing `.py` (use with `--asm/--dump`)
|
|
48
|
-
- `--skip-path` flatten output paths (write next to input)
|
|
49
|
-
- `--out` print source to stdout instead of files
|
|
50
|
-
- `--marshal` treat input as raw marshalled data (no .pyc header, auto-scan versions)
|
|
51
|
-
- `--marshal-scan` fast scan marshal blobs and print version candidates
|
|
52
|
-
- `--py-version <x.y>` bytecode version hint (use with `--marshal`)
|
|
53
|
-
- `--basedir <dir>` override output root (default: alongside input)
|
|
54
|
-
- `--file-ext <ext>` change emitted extension (default `py`)
|
|
55
|
-
|
|
56
|
-
## Examples
|
|
57
|
-
- Disassemble only (no source): `node depyo.js --skip-source-gen --asm file.pyc`
|
|
58
|
-
- Keep raw + disassembly next to source: `node depyo.js --raw --asm path/to/file.pyc`
|
|
59
|
-
- Flatten outputs (helpful for bulk zips): `node depyo.js --skip-path archive.zip`
|
|
65
|
+
## Example
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
67
|
+
Input `greet.py`:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
async def greet(names: list[str], *, greeting: str = "Hello") -> None:
|
|
71
|
+
seen = set()
|
|
72
|
+
for name in names:
|
|
73
|
+
if name in seen:
|
|
74
|
+
continue
|
|
75
|
+
seen.add(name)
|
|
76
|
+
print(f"{greeting}, {name}!")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Compile (`python3.13 -c 'import py_compile; py_compile.compile("greet.py", "greet.pyc")'`) then:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
$ npx depyo --out greet.pyc
|
|
83
|
+
async def greet(names: list[str], *, greeting: str = "Hello") -> None:
|
|
84
|
+
seen = set()
|
|
85
|
+
for name in names:
|
|
86
|
+
if name in seen:
|
|
87
|
+
continue
|
|
88
|
+
seen.add(name)
|
|
89
|
+
print(f"{greeting}, {name}!")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Pattern matching round-trips too:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
match command.split():
|
|
96
|
+
case [action]:
|
|
97
|
+
run(action)
|
|
98
|
+
case [action, obj] if action in VERBS:
|
|
99
|
+
run(action, obj)
|
|
100
|
+
case _:
|
|
101
|
+
print("usage: ...")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## CLI options
|
|
105
|
+
|
|
106
|
+
| Option | Effect |
|
|
107
|
+
| ------------------------ | --------------------------------------------------------------- |
|
|
108
|
+
| `--asm` | Emit `.pyasm` disassembly alongside source |
|
|
109
|
+
| `--raw` | Copy raw `.pyc` next to output |
|
|
110
|
+
| `--raw-spacing` | Preserve blank-line / comment gaps |
|
|
111
|
+
| `--dump` | Dump the marshalled object tree |
|
|
112
|
+
| `--stats` | Print throughput stats |
|
|
113
|
+
| `--skip-source-gen` | Skip writing `.py` (useful with `--asm`/`--dump`) |
|
|
114
|
+
| `--skip-path` | Flatten output paths (write next to input) |
|
|
115
|
+
| `--out` | Print source to stdout instead of files |
|
|
116
|
+
| `--marshal` | Treat input as raw marshalled data (no `.pyc` header) |
|
|
117
|
+
| `--marshal-scan` | Fast scan marshal blobs; print candidate versions |
|
|
118
|
+
| `--py-version <x.y>` | Bytecode version hint (required for some headerless marshals) |
|
|
119
|
+
| `--basedir <dir>` | Override output root (default: alongside input) |
|
|
120
|
+
| `--file-ext <ext>` | Change emitted extension (default `py`) |
|
|
121
|
+
|
|
122
|
+
## Programmatic API
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
const {PycReader} = require('depyo/lib/PycReader');
|
|
126
|
+
const {PycDecompiler} = require('depyo/lib/PycDecompiler');
|
|
127
|
+
|
|
128
|
+
const fs = require('fs');
|
|
129
|
+
const buffer = fs.readFileSync('greet.pyc');
|
|
130
|
+
const reader = new PycReader(buffer);
|
|
131
|
+
const obj = reader.ReadObject();
|
|
132
|
+
|
|
133
|
+
const decompiler = new PycDecompiler(obj);
|
|
134
|
+
const ast = decompiler.decompile();
|
|
135
|
+
console.log(ast.codeFragment().toString());
|
|
136
|
+
```
|
|
81
137
|
|
|
82
138
|
## Support matrix
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
139
|
+
|
|
140
|
+
- **Python 1.0–3.15** opcode tables and expected fixtures.
|
|
141
|
+
- **Modern features:** match/case (guards, OR-patterns, bindings, wildcards), walrus, f-strings (nested, equals-sign debug), exception groups (`except*`), async comprehensions, type parameters, PEP 696 TypeVar defaults, PEP 750 t-strings.
|
|
142
|
+
- **PyPy** bytecode decompiles; expected fixtures not yet part of CI.
|
|
143
|
+
- **CI gates:** Modern feature checks are blocking; legacy 1.x / 2.7 / 3.0–3.6 smokes gate as well.
|
|
87
144
|
|
|
88
145
|
## Known limitations
|
|
89
|
-
- **Inline comprehensions (Python 3.12+):** PEP 709 inlines list/dict/set comprehensions into parent code objects. Depyo currently reconstructs these as for-loops rather than comprehension expressions. Functions, classes, match/case, exception handling, and other constructs work correctly.
|
|
90
146
|
|
|
91
|
-
|
|
147
|
+
- **Inline comprehensions (3.12+):** PEP 709 inlines list/dict/set comprehensions into the parent code object. Depyo currently reconstructs these as for-loops rather than comprehension expressions. Functions, classes, match/case, exception handling, and other constructs work correctly.
|
|
148
|
+
- **Comments / blank lines:** Lost in compilation and not recoverable. `--raw-spacing` can hint at original gaps using line-number attributes.
|
|
149
|
+
- **Source-level AST drift:** Some constructs are normalized by CPython before bytecode (e.g. `if not x: raise AssertionError` ↔ `assert x`). Depyo renders what the compiler produced.
|
|
150
|
+
|
|
151
|
+
## Testing
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# Smoke per version
|
|
155
|
+
node scripts/run-fixtures.js --root test/bytecode_3.14 --pattern py314_with_except_star --fail-fast
|
|
156
|
+
node scripts/run-fixtures.js --root test/bytecode_3.6 --pattern py36_fstrings --fail-fast
|
|
157
|
+
|
|
158
|
+
# Full matrix
|
|
159
|
+
node scripts/run-matrix.js
|
|
160
|
+
node scripts/run-matrix.js --pattern py311_exception_groups --fail-fast
|
|
161
|
+
|
|
162
|
+
# Marshal-blob fixtures (headerless)
|
|
163
|
+
node scripts/run-marshal-fixtures.js
|
|
164
|
+
|
|
165
|
+
# Regenerate snapshot fixtures (destructive)
|
|
166
|
+
node scripts/generate-marshal-fixtures.js --clean
|
|
167
|
+
|
|
168
|
+
# Tier-1 oracle: parseability of every decompiled fixture
|
|
169
|
+
node scripts/check-parseable.js
|
|
170
|
+
|
|
171
|
+
# Tier-2 oracle: AST equivalence between source .py and decompiled .py
|
|
172
|
+
node scripts/check-ast-equivalence.js
|
|
173
|
+
|
|
174
|
+
# Sentinel leak gate (CI-critical)
|
|
175
|
+
node scripts/check-no-sentinels.js
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Modern fixtures are generated via `test/generate_modern_tests.py` (Python 3.8+ on PATH).
|
|
179
|
+
|
|
180
|
+
## Contributing
|
|
181
|
+
|
|
92
182
|
- Use `node scripts/run-fixtures.js --pattern <piece>` for fast repros.
|
|
93
183
|
- For full coverage, `node scripts/run-matrix.js --fail-fast` (optionally add `--pattern`).
|
|
94
|
-
-
|
|
184
|
+
- `--raw-spacing` helps inspect potential comment/blank-line gaps.
|
|
95
185
|
- `--stats` helps when profiling throughput.
|
|
96
186
|
|
|
97
|
-
|
|
187
|
+
Issues, repro `.pyc` files, and PRs welcome at https://github.com/skuznetsov/depyo.js/issues.
|
|
98
188
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
| Project | Supported versions | Modern features (match, walrus, f-strings, exc groups) | Delivery | Expected fixtures | Notes |
|
|
102
|
-
| ------------------ | --------------------------- | ------------------------------------------------------ | ------------ | ----------------- | ----------------------------------------- |
|
|
103
|
-
| depyo | 1.0–3.14 (PyPy decompiles) | Yes | npm/npx, CLI | Yes (1.0–3.14) | Node.js CLI, asm/raw-spacing options |
|
|
104
|
-
| uncompyle6/decompyle3 | 2.x–3.12+ (lag on 3.13/3.14) | Partial (depends on branch) | pip | Partial | Python-based, slower adoption of new ops |
|
|
105
|
-
| pycdc (C++) | Mostly 2.x–3.x (limited new) | Partial | source build | No | Fast, but modern coverage limited |
|
|
189
|
+
Comments and docs are in English; output mirrors the target Python version syntax.
|
|
106
190
|
|
|
107
|
-
##
|
|
108
|
-
- Machine: local Node 25, single-thread.
|
|
109
|
-
- Case: `py314_exception_groups.pyc` decompiled 50× in-process: ~5.3 ms total (≈0.1 ms per decompile).
|
|
110
|
-
Use `node depyo.js --stats <file.pyc>` for your environment.
|
|
191
|
+
## License
|
|
111
192
|
|
|
112
|
-
|
|
113
|
-
- Announce on HN/Reddit (Show HN / r/Python) with npm/npx one-liners.
|
|
114
|
-
- Add to awesome lists (`awesome-python`, `awesome-reverse-engineering`).
|
|
115
|
-
- Provide asciinema/GIF of `npx depyo file.pyc` + `--asm`.
|
|
116
|
-
- Encourage contributions via Issues/Discussions and `help wanted` labels.
|
|
193
|
+
MIT — see [LICENSE](LICENSE).
|
package/lib/PycDecompiler.js
CHANGED
|
@@ -933,6 +933,23 @@ class PycDecompiler {
|
|
|
933
933
|
break; // This WITH block hasn't reached its end yet
|
|
934
934
|
}
|
|
935
935
|
|
|
936
|
+
// Close any blocks ABOVE this WITH whose end is also reached.
|
|
937
|
+
// 3.14 with-bodies that contain an if/else commonly have the
|
|
938
|
+
// else's end coincide with the with's end (both target the
|
|
939
|
+
// post-with cleanup offset). If we close the WITH while an
|
|
940
|
+
// Else is still open above it, the else body gets reparented
|
|
941
|
+
// to the WITH's parent — producing a stray `else:` dedented
|
|
942
|
+
// out of the with-block in the rendered source.
|
|
943
|
+
while (this.blocks.length - 1 > withIdx) {
|
|
944
|
+
const above = this.blocks.top();
|
|
945
|
+
if (above.end <= 0 || this.code.Current.Offset < above.end) {
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
this.blocks.pop();
|
|
949
|
+
const newTop = this.blocks.top();
|
|
950
|
+
if (newTop) newTop.append(above);
|
|
951
|
+
}
|
|
952
|
+
|
|
936
953
|
// Close this WITH block
|
|
937
954
|
this.blocks.splice(withIdx, 1);
|
|
938
955
|
const parentBlock = this.blocks[withIdx - 1] || this.blocks.top();
|
|
@@ -1914,9 +1931,16 @@ class PycDecompiler {
|
|
|
1914
1931
|
// trailing compiler-generated reraise/cleanup nodes (bare `raise`,
|
|
1915
1932
|
// `e = None; del e`, empty `try: pass`). Runs once on the whole tree.
|
|
1916
1933
|
cleanupExcMatchArtifacts(root) {
|
|
1917
|
-
//
|
|
1918
|
-
//
|
|
1919
|
-
|
|
1934
|
+
// Pre-3.11 except-as handlers still need two cleanups: strip the
|
|
1935
|
+
// `X = None; del X` pair CPython emits after every `except ... as X:`
|
|
1936
|
+
// body (PEP 3134), and unwrap the synthetic `try: body / finally: pass`
|
|
1937
|
+
// that wraps the handler body (CPython lowers `except ... as X:` to
|
|
1938
|
+
// `try: body; finally: X = None; del X`, and the finally body ends up
|
|
1939
|
+
// emptied as the X=None/del X move out to the handler tail).
|
|
1940
|
+
if (this.object.Reader.versionCompare(3, 11) < 0) {
|
|
1941
|
+
this.cleanupPre311ExceptAsArtifacts(root);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1920
1944
|
|
|
1921
1945
|
const EXC_MATCH = AST.ASTCompare.CompareOp.Exception;
|
|
1922
1946
|
|
|
@@ -2354,6 +2378,95 @@ class PycDecompiler {
|
|
|
2354
2378
|
visit(root);
|
|
2355
2379
|
}
|
|
2356
2380
|
|
|
2381
|
+
// Pre-3.11 `except Foo as X:` expands to `try: body / finally: X = None;
|
|
2382
|
+
// del X`. The decompiler hoists the cleanup out to the handler tail,
|
|
2383
|
+
// leaving `try: body / finally: pass / X = None / del X`. Strip the pair
|
|
2384
|
+
// and unwrap the synthetic try/finally so the handler renders as `body`.
|
|
2385
|
+
cleanupPre311ExceptAsArtifacts(root) {
|
|
2386
|
+
const isNoneDelPair = (a, b) =>
|
|
2387
|
+
a instanceof AST.ASTStore &&
|
|
2388
|
+
a.src instanceof AST.ASTNone &&
|
|
2389
|
+
a.dest instanceof AST.ASTName &&
|
|
2390
|
+
b instanceof AST.ASTDelete &&
|
|
2391
|
+
b.value instanceof AST.ASTName &&
|
|
2392
|
+
a.dest.name === b.value.name;
|
|
2393
|
+
|
|
2394
|
+
const isFinallyTrivial = (n) =>
|
|
2395
|
+
n instanceof AST.ASTBlock &&
|
|
2396
|
+
n.blockType === AST.ASTBlock.BlockType.Finally &&
|
|
2397
|
+
(n.nodes.length === 0 ||
|
|
2398
|
+
(n.nodes.length === 1 &&
|
|
2399
|
+
n.nodes[0] instanceof AST.ASTKeyword &&
|
|
2400
|
+
n.nodes[0].key === AST.ASTKeyword.Word.Pass));
|
|
2401
|
+
|
|
2402
|
+
// A CPython `except Foo as X:` body wraps user code in a synthetic
|
|
2403
|
+
// `try: body / finally: X=None; del X`. After the Pass 3.5 strip the
|
|
2404
|
+
// finally becomes empty / pass-only, leaving an ASTContainerBlock
|
|
2405
|
+
// whose children are [Try(body), Finally(pass)]. Collapse that back
|
|
2406
|
+
// to just the try-body so the handler renders as `body`.
|
|
2407
|
+
const unwrapTryFinallyPassContainer = (nodes) => {
|
|
2408
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2409
|
+
const cont = nodes[i];
|
|
2410
|
+
if (!(cont instanceof AST.ASTContainerBlock)) continue;
|
|
2411
|
+
const children = cont.nodes;
|
|
2412
|
+
if (!children || children.length !== 2) continue;
|
|
2413
|
+
const tryBlk = children[0];
|
|
2414
|
+
const finBlk = children[1];
|
|
2415
|
+
if (!(tryBlk instanceof AST.ASTBlock) ||
|
|
2416
|
+
tryBlk.blockType !== AST.ASTBlock.BlockType.Try) continue;
|
|
2417
|
+
if (!isFinallyTrivial(finBlk)) continue;
|
|
2418
|
+
if (tryBlk.nodes.length === 0) continue;
|
|
2419
|
+
// Keep real user try/except nested inside — unwrapping
|
|
2420
|
+
// would change semantics when the try body itself carries
|
|
2421
|
+
// handlers that could fire.
|
|
2422
|
+
const hasNestedHandler = tryBlk.nodes.some(n =>
|
|
2423
|
+
n instanceof AST.ASTCondBlock &&
|
|
2424
|
+
(n.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2425
|
+
n.blockType === AST.ASTBlock.BlockType.Finally));
|
|
2426
|
+
if (hasNestedHandler) continue;
|
|
2427
|
+
nodes.splice(i, 1, ...tryBlk.nodes);
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2430
|
+
|
|
2431
|
+
const stripNoneDelPairs = (nodes) => {
|
|
2432
|
+
for (let i = nodes.length - 2; i >= 0; i--) {
|
|
2433
|
+
if (isNoneDelPair(nodes[i], nodes[i + 1])) {
|
|
2434
|
+
nodes.splice(i, 2);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2438
|
+
|
|
2439
|
+
const visited = new WeakSet();
|
|
2440
|
+
const visit = (n) => {
|
|
2441
|
+
if (!n || visited.has(n)) return;
|
|
2442
|
+
visited.add(n);
|
|
2443
|
+
let nodes = null;
|
|
2444
|
+
let parentType = null;
|
|
2445
|
+
if (n instanceof AST.ASTNodeList) {
|
|
2446
|
+
nodes = n.list;
|
|
2447
|
+
} else if (n instanceof AST.ASTBlock) {
|
|
2448
|
+
nodes = n.nodes;
|
|
2449
|
+
parentType = n.blockType;
|
|
2450
|
+
} else if (n instanceof AST.ASTStore && n.src instanceof AST.ASTFunction) {
|
|
2451
|
+
const body = n.src.code?.object?.SourceCode;
|
|
2452
|
+
if (body) visit(body);
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
if (nodes) {
|
|
2456
|
+
for (const c of nodes) visit(c);
|
|
2457
|
+
stripNoneDelPairs(nodes);
|
|
2458
|
+
// The try/finally:pass wrapper is CPython's synthetic scaffold
|
|
2459
|
+
// for `except Foo as X:` — only unwrap inside Except bodies,
|
|
2460
|
+
// otherwise real user-written `try: body / finally: pass`
|
|
2461
|
+
// constructs at top-level/function body get collapsed too.
|
|
2462
|
+
if (parentType === AST.ASTBlock.BlockType.Except) {
|
|
2463
|
+
unwrapTryFinallyPassContainer(nodes);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
};
|
|
2467
|
+
visit(root);
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2357
2470
|
// An Except block can get reconstructed inside a non-Try block's body
|
|
2358
2471
|
// (most often a For/While/If inside the protected try-body) when the
|
|
2359
2472
|
// handler's offset range overlaps a nested loop. Python requires the
|
package/lib/PycReader.js
CHANGED
|
@@ -38,13 +38,14 @@ const TypeSmallTuple = ')';
|
|
|
38
38
|
const TypeShortAscii = 'z';
|
|
39
39
|
const TypeShortAsciiInterned = 'Z';
|
|
40
40
|
const TypeObjectReference = 'r';
|
|
41
|
+
const TypeSlice = ':'; // CPython 3.14+: marshal'd slice constant (start, stop, step)
|
|
41
42
|
const KnownTypes = new Set([
|
|
42
43
|
TypeNull, TypeNone, TypeFalse, TypeTrue, TypeStopiter, TypeEllipsis,
|
|
43
44
|
TypeInt, TypeInt64, TypeFloat, TypeBinaryFloat, TypeComplex, TypeBinaryComplex,
|
|
44
45
|
TypeLong, TypeString, TypeInterned, TypeStringRef, TypeTuple, TypeList,
|
|
45
46
|
TypeDict, TypeCode, TypeCode2, TypeUnicode, TypeUnknown, TypeSet, TypeFrozenset,
|
|
46
47
|
TypeAscii, TypeAsciiInterned, TypeSmallTuple, TypeShortAscii, TypeShortAsciiInterned,
|
|
47
|
-
TypeObjectReference
|
|
48
|
+
TypeObjectReference, TypeSlice
|
|
48
49
|
]);
|
|
49
50
|
|
|
50
51
|
const MagicToVersion = {
|
|
@@ -590,6 +591,14 @@ class PycReader
|
|
|
590
591
|
obj.ClassName = "Py_FrozenSet";
|
|
591
592
|
obj.Value = frozenSet;
|
|
592
593
|
break;
|
|
594
|
+
case TypeSlice: {
|
|
595
|
+
const sliceStart = this.ReadObject();
|
|
596
|
+
const sliceStop = this.ReadObject();
|
|
597
|
+
const sliceStep = this.ReadObject();
|
|
598
|
+
obj.ClassName = "Py_Slice";
|
|
599
|
+
obj.Value = { start: sliceStart, stop: sliceStop, step: sliceStep };
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
593
602
|
case TypeCode:
|
|
594
603
|
case TypeCode2:
|
|
595
604
|
let codeObj = this.ReadCodeObject();
|
package/lib/PythonObject.js
CHANGED
|
@@ -194,7 +194,13 @@ class PythonObject {
|
|
|
194
194
|
|
|
195
195
|
case "Py_Complex":
|
|
196
196
|
return `${this.Value[0]}+${this.Value[1]}j`;
|
|
197
|
-
|
|
197
|
+
|
|
198
|
+
case "Py_Slice": {
|
|
199
|
+
const part = (v) => v instanceof PythonObject ? v.toReprString() : String(v);
|
|
200
|
+
const { start, stop, step } = this.Value || {};
|
|
201
|
+
return `slice(${part(start)}, ${part(stop)}, ${part(step)})`;
|
|
202
|
+
}
|
|
203
|
+
|
|
198
204
|
default:
|
|
199
205
|
return ["Py_String"].includes(this.ClassName) ? this.Value.toString("ascii") : null;
|
|
200
206
|
}
|
package/lib/ast/ast_node.js
CHANGED
|
@@ -212,9 +212,14 @@ class ASTNodeList extends ASTNode {
|
|
|
212
212
|
if (this.emptyBlock()) {
|
|
213
213
|
result.add("pass");
|
|
214
214
|
} else {
|
|
215
|
+
// Defensive: a handler may have appended an undefined slot (e.g.
|
|
216
|
+
// a stack-pop that returned undefined on a malformed pyc).
|
|
217
|
+
// Without this, the sibling-link loop crashes on `undefined.prevSibling`
|
|
218
|
+
// and aborts rendering of the whole code object.
|
|
219
|
+
const list = this.list.filter(n => n);
|
|
215
220
|
let prevNode = null;
|
|
216
221
|
|
|
217
|
-
for (let node of
|
|
222
|
+
for (let node of list) {
|
|
218
223
|
if (prevNode) {
|
|
219
224
|
prevNode.nextSibling = node;
|
|
220
225
|
}
|
|
@@ -223,7 +228,7 @@ class ASTNodeList extends ASTNode {
|
|
|
223
228
|
}
|
|
224
229
|
prevNode = null;
|
|
225
230
|
|
|
226
|
-
for (let node of
|
|
231
|
+
for (let node of list) {
|
|
227
232
|
if (node.skip) {
|
|
228
233
|
continue;
|
|
229
234
|
}
|
|
@@ -2602,6 +2607,17 @@ class ASTBlock extends ASTNode {
|
|
|
2602
2607
|
}
|
|
2603
2608
|
}
|
|
2604
2609
|
|
|
2610
|
+
// Predicate: can this node legally appear as one branch of a ternary `x if c else y`?
|
|
2611
|
+
// Block-shaped nodes render multi-line and an ASTKeyword (pass/break/continue) is a
|
|
2612
|
+
// statement, not an expression — appending " if c else y" to either is invalid Python.
|
|
2613
|
+
function isTernaryExprNode(node) {
|
|
2614
|
+
if (node == null) return false;
|
|
2615
|
+
if (node instanceof ASTBlock) return false;
|
|
2616
|
+
if (node instanceof ASTKeyword) return false;
|
|
2617
|
+
if (node instanceof ASTReturn) return false;
|
|
2618
|
+
return true;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2605
2621
|
class ASTCondBlock extends ASTBlock {
|
|
2606
2622
|
static InitCondition = {
|
|
2607
2623
|
Uninited: 0,
|
|
@@ -2636,7 +2652,23 @@ class ASTCondBlock extends ASTBlock {
|
|
|
2636
2652
|
|
|
2637
2653
|
codeFragment() {
|
|
2638
2654
|
let result = new PycResult("", true);
|
|
2655
|
+
// Cycle guard: if a control-flow handler accidentally nests a block
|
|
2656
|
+
// inside its own descendants, the recursive render would blow the JS
|
|
2657
|
+
// stack and abort the enclosing code object. Detect re-entry on the
|
|
2658
|
+
// same instance, emit a sentinel, and let the rest of the file render.
|
|
2659
|
+
if (this.__rendering) {
|
|
2660
|
+
result.add("###CYCLE###");
|
|
2661
|
+
return result;
|
|
2662
|
+
}
|
|
2663
|
+
this.__rendering = true;
|
|
2664
|
+
try {
|
|
2665
|
+
return this._codeFragmentImpl(result);
|
|
2666
|
+
} finally {
|
|
2667
|
+
this.__rendering = false;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2639
2670
|
|
|
2671
|
+
_codeFragmentImpl(result) {
|
|
2640
2672
|
// This is the assert case
|
|
2641
2673
|
if (this.blockType == ASTBlock.BlockType.If && this.negative && this.condition && this.nodes.length == 1 && this.nodes[0] instanceof ASTRaise) {
|
|
2642
2674
|
result.lastLineAppend("assert ", false);
|
|
@@ -2651,9 +2683,11 @@ class ASTCondBlock extends ASTBlock {
|
|
|
2651
2683
|
this.prevSibling instanceof ASTStore &&
|
|
2652
2684
|
this.blockType == ASTBlock.BlockType.If &&
|
|
2653
2685
|
this.nodes.length == 1 &&
|
|
2686
|
+
isTernaryExprNode(this.nodes[0]) &&
|
|
2654
2687
|
this.nextSibling instanceof ASTCondBlock &&
|
|
2655
2688
|
this.nextSibling?.type == ASTBlock.BlockType.Else &&
|
|
2656
2689
|
this.nextSibling?.nodes.length == 1 &&
|
|
2690
|
+
isTernaryExprNode(this.nextSibling.nodes[0]) &&
|
|
2657
2691
|
this.condition.line >= 0 &&
|
|
2658
2692
|
this.condition.line == this.nodes[0].line &&
|
|
2659
2693
|
this.condition.line == this.nextSibling?.nodes[0].line
|
|
@@ -2670,7 +2704,10 @@ class ASTCondBlock extends ASTBlock {
|
|
|
2670
2704
|
this.prevSibling == null &&
|
|
2671
2705
|
this.blockType == ASTBlock.BlockType.If &&
|
|
2672
2706
|
this.nodes.length == 1 &&
|
|
2707
|
+
isTernaryExprNode(this.nodes[0]) &&
|
|
2673
2708
|
this.nextSibling instanceof ASTReturn &&
|
|
2709
|
+
this.nextSibling.value &&
|
|
2710
|
+
isTernaryExprNode(this.nextSibling.value) &&
|
|
2674
2711
|
this.condition.line == this.nodes[0].line &&
|
|
2675
2712
|
this.condition.line == this.nextSibling?.value.line
|
|
2676
2713
|
) {
|
|
@@ -7,7 +7,7 @@ const opcodes = [
|
|
|
7
7
|
[0, new OpCode(OpCodes.CACHE, "CACHE")],
|
|
8
8
|
[1, new OpCode(OpCodes.BINARY_SLICE, "BINARY_SLICE")],
|
|
9
9
|
[2, new OpCode(OpCodes.BUILD_TEMPLATE, "BUILD_TEMPLATE")],
|
|
10
|
-
[4, new OpCode(OpCodes.
|
|
10
|
+
[4, new OpCode(OpCodes.CALL_FUNCTION_EX_A, "CALL_FUNCTION_EX", {HasArgument: true})],
|
|
11
11
|
[5, new OpCode(OpCodes.CHECK_EG_MATCH, "CHECK_EG_MATCH")],
|
|
12
12
|
[6, new OpCode(OpCodes.CHECK_EXC_MATCH, "CHECK_EXC_MATCH")],
|
|
13
13
|
[7, new OpCode(OpCodes.CLEANUP_THROW, "CLEANUP_THROW")],
|
|
@@ -2,6 +2,15 @@ const AST = require('../ast/ast_node');
|
|
|
2
2
|
|
|
3
3
|
function handleBinaryOpA()
|
|
4
4
|
{
|
|
5
|
+
// Python 3.14 fused BINARY_SUBSCR into BINARY_OP arg=26 (NB_SUBSCR).
|
|
6
|
+
if (this.code.Current.Argument === 26) {
|
|
7
|
+
let subscr = this.dataStack.pop();
|
|
8
|
+
let src = this.dataStack.pop();
|
|
9
|
+
let node = new AST.ASTSubscr(src, subscr);
|
|
10
|
+
node.line = this.code.Current.LineNo;
|
|
11
|
+
this.dataStack.push(node);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
5
14
|
let rVal = this.dataStack.pop();
|
|
6
15
|
let lVal = this.dataStack.pop();
|
|
7
16
|
let op = AST.ASTBinary.from_binary_op(this.code.Current.Argument);
|
|
@@ -182,6 +182,22 @@ function handleLoadGlobalA() {
|
|
|
182
182
|
this.dataStack.push(node);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
function handleLoadFromDictOrGlobalsA() {
|
|
186
|
+
// Python 3.12+: pops the class-body locals dict pushed by LOAD_LOCALS,
|
|
187
|
+
// looks up co_names[A] there, falling back to globals. For source
|
|
188
|
+
// reconstruction we render it as a bare name reference.
|
|
189
|
+
if (this.dataStack.top() instanceof AST.ASTLocals) {
|
|
190
|
+
this.dataStack.pop();
|
|
191
|
+
}
|
|
192
|
+
const varName = this.code.Current.Name || "";
|
|
193
|
+
if (!varName.length) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
let node = new AST.ASTName(varName);
|
|
197
|
+
node.line = this.code.Current.LineNo;
|
|
198
|
+
this.dataStack.push(node);
|
|
199
|
+
}
|
|
200
|
+
|
|
185
201
|
function handleLoadFromDictOrDerefA() {
|
|
186
202
|
// Python 3.12+: LOAD_FROM_DICT_OR_DEREF consumes the locals dict from TOS
|
|
187
203
|
// (pushed by the preceding LOAD_LOCALS) and resolves the name against it,
|
|
@@ -253,9 +269,15 @@ function handleLoadSpecialA() {
|
|
|
253
269
|
}
|
|
254
270
|
|
|
255
271
|
// For __exit__ (oparg 1): save the context manager expression for with-block
|
|
256
|
-
// This is called BEFORE __enter__ in 3.14 bytecode pattern
|
|
272
|
+
// This is called BEFORE __enter__ in 3.14 bytecode pattern.
|
|
273
|
+
// The 3.14 with-prologue uses LOAD_FAST(ctx); COPY 1; LOAD_SPECIAL 1, which
|
|
274
|
+
// looks like the LOAD+COPY 1 match-subject idiom — clear the candidate so
|
|
275
|
+
// a later COMPARE_OP inside the with-body doesn't get wrapped in a
|
|
276
|
+
// synthetic `match ctx:` rooted on the context manager.
|
|
257
277
|
if (oparg === 1) {
|
|
258
278
|
this._py314WithContextMgr = obj;
|
|
279
|
+
this.potentialMatchSubject = null;
|
|
280
|
+
this.matchCandidateStart = -1;
|
|
259
281
|
}
|
|
260
282
|
|
|
261
283
|
// For __enter__ (oparg 0): create with-block using saved context manager
|
|
@@ -478,6 +500,28 @@ function processStore(nameOverride) {
|
|
|
478
500
|
}
|
|
479
501
|
}
|
|
480
502
|
|
|
503
|
+
// `case _ as name:` final clause: no preceding pattern ops, no LOAD
|
|
504
|
+
// before the STORE, and we're inside a match. CPython emits a bare STORE
|
|
505
|
+
// to bind the subject still living on the stack.
|
|
506
|
+
if (this.currentMatch && !this.currentCase && !this.inMatchPattern &&
|
|
507
|
+
(this.patternOps?.length || 0) === 0) {
|
|
508
|
+
const prevId = this.code.Prev?.OpCodeID;
|
|
509
|
+
const isAfterReturn = prevId == this.OpCodes.RETURN_VALUE ||
|
|
510
|
+
prevId == this.OpCodes.RETURN_VALUE_A ||
|
|
511
|
+
prevId == this.OpCodes.RETURN_CONST_A;
|
|
512
|
+
if (isAfterReturn) {
|
|
513
|
+
const wildcard = new AST.ASTPattern(AST.ASTPattern.PatternType.Wildcard, '_');
|
|
514
|
+
const asPattern = new AST.ASTPattern(AST.ASTPattern.PatternType.As, {
|
|
515
|
+
pattern: wildcard,
|
|
516
|
+
name: currentName
|
|
517
|
+
});
|
|
518
|
+
this.currentCase = new AST.ASTCase(asPattern, null, null);
|
|
519
|
+
this.currentCase.line = this.code.Current.LineNo;
|
|
520
|
+
this.caseBodyStartIndex = this.curBlock?.nodes?.length || 0;
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
481
525
|
if (global.g_cliArgs?.debug && currentName && (currentName === 'b' || currentName === 'i')) {
|
|
482
526
|
console.log(`[processStore] varName=${currentName}, curBlock=${this.curBlock.type_str}, inited=${this.curBlock.inited}, unpack=${this.unpack}`);
|
|
483
527
|
console.log(` Block stack: ${this.blocks.map((b,i) => `[${i}]${b.type_str}(inited=${b.inited})`).join(' → ')}`);
|
|
@@ -832,6 +876,7 @@ module.exports = {
|
|
|
832
876
|
handleLoadZeroSuperAttrA,
|
|
833
877
|
handleLoadZeroSuperMethodA,
|
|
834
878
|
handleLoadFromDictOrDerefA,
|
|
879
|
+
handleLoadFromDictOrGlobalsA,
|
|
835
880
|
handleStoreAttrA,
|
|
836
881
|
handleStoreDerefA,
|
|
837
882
|
handleStoreFastA,
|
|
@@ -241,11 +241,25 @@ function reconstructPattern(patternOps) {
|
|
|
241
241
|
));
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
// Literal OR pattern: multiple COMPARE_OP checks in a row
|
|
244
|
+
// Literal OR pattern: multiple COMPARE_OP checks in a row.
|
|
245
|
+
// Pattern compares share the same `left` (the matched subject). Anything
|
|
246
|
+
// with a different left is a guard expression that bled into patternOps.
|
|
245
247
|
const allCompareOps = ops.filter(op => op.type === 'COMPARE');
|
|
246
|
-
|
|
248
|
+
const sameLeft = (a, b) => {
|
|
249
|
+
if (a === b) return true;
|
|
250
|
+
if (!a || !b) return false;
|
|
251
|
+
const af = a.codeFragment?.()?.toString?.();
|
|
252
|
+
const bf = b.codeFragment?.()?.toString?.();
|
|
253
|
+
return !!af && af === bf;
|
|
254
|
+
};
|
|
255
|
+
let patternCompareOps = allCompareOps;
|
|
256
|
+
if (allCompareOps.length > 1) {
|
|
257
|
+
const subjectLeft = allCompareOps[0].left;
|
|
258
|
+
patternCompareOps = allCompareOps.filter(op => sameLeft(op.left, subjectLeft));
|
|
259
|
+
}
|
|
260
|
+
if (!hasMatchSeq && patternCompareOps.length > 1) {
|
|
247
261
|
const orPatterns = [];
|
|
248
|
-
for (const cmp of
|
|
262
|
+
for (const cmp of patternCompareOps) {
|
|
249
263
|
markConsumed(cmp);
|
|
250
264
|
if (cmp.right instanceof AST.ASTObject) {
|
|
251
265
|
orPatterns.push(new AST.ASTPattern(AST.ASTPattern.PatternType.Literal, cmp.right));
|
|
@@ -366,6 +380,21 @@ function hasUpcomingMatchCase() {
|
|
|
366
380
|
return true;
|
|
367
381
|
}
|
|
368
382
|
|
|
383
|
+
// `case _ as name:` final clause — CPython emits a bare STORE for the
|
|
384
|
+
// binding right after the previous case's RETURN. No LOAD precedes it
|
|
385
|
+
// (the stored value is the matched subject preserved on the stack).
|
|
386
|
+
if (this.currentMatch && this.matchSubject &&
|
|
387
|
+
(nextOpCodeId == this.OpCodes.STORE_FAST_A ||
|
|
388
|
+
nextOpCodeId == this.OpCodes.STORE_NAME_A)) {
|
|
389
|
+
const prevId = this.code.Current.OpCodeID;
|
|
390
|
+
const isAfterReturn = prevId == this.OpCodes.RETURN_VALUE ||
|
|
391
|
+
prevId == this.OpCodes.RETURN_VALUE_A ||
|
|
392
|
+
prevId == this.OpCodes.RETURN_CONST_A;
|
|
393
|
+
if (isAfterReturn) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
369
398
|
// NOP followed by code (no COMPARE_OP) = default case (case _:)
|
|
370
399
|
// This pattern appears in 3.10/3.11 after the last literal case
|
|
371
400
|
if (nextOpCodeId == this.OpCodes.NOP && this.currentMatch) {
|
|
@@ -1330,6 +1359,12 @@ function handleInstrumentedLineA() {
|
|
|
1330
1359
|
}
|
|
1331
1360
|
}
|
|
1332
1361
|
|
|
1362
|
+
// CPython 3.13+: POP_ITER replaces the post-loop POP_TOP that discards an
|
|
1363
|
+
// exhausted iterator. Semantically identical to POP_TOP for our purposes.
|
|
1364
|
+
function handlePopIter() {
|
|
1365
|
+
return handlePopTop.call(this);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1333
1368
|
module.exports = {
|
|
1334
1369
|
beginMatchCaseFromPattern,
|
|
1335
1370
|
flushCurrentCaseBody,
|
|
@@ -1338,6 +1373,7 @@ module.exports = {
|
|
|
1338
1373
|
handleFormatSimple,
|
|
1339
1374
|
handleFormatWithSpec,
|
|
1340
1375
|
handlePopTop,
|
|
1376
|
+
handlePopIter,
|
|
1341
1377
|
handlePrintExpr,
|
|
1342
1378
|
handlePrintItem,
|
|
1343
1379
|
handlePrintItemTo,
|
|
@@ -28,7 +28,8 @@ function handleDupTop() {
|
|
|
28
28
|
|
|
29
29
|
// Look ahead to confirm match pattern immediately
|
|
30
30
|
if (!this.currentMatch && this.lookAheadForMatchPattern()) {
|
|
31
|
-
this.
|
|
31
|
+
this.matchSubject = this.potentialMatchSubject;
|
|
32
|
+
this.currentMatch = new AST.ASTMatch(this.matchSubject);
|
|
32
33
|
this.currentMatch.line = this.code.Current.LineNo;
|
|
33
34
|
this.matchParentBlock = this.curBlock;
|
|
34
35
|
this.inMatchPattern = true; // Start tracking pattern operations
|
|
@@ -46,7 +47,8 @@ function handleDupTop() {
|
|
|
46
47
|
if (!this.currentMatch && this.potentialMatchSubject &&
|
|
47
48
|
this.object.Reader.versionCompare(3, 10) >= 0) {
|
|
48
49
|
// First time - create match (fallback if look-ahead didn't work)
|
|
49
|
-
this.
|
|
50
|
+
this.matchSubject = this.potentialMatchSubject;
|
|
51
|
+
this.currentMatch = new AST.ASTMatch(this.matchSubject);
|
|
50
52
|
this.currentMatch.line = this.code.Current.LineNo;
|
|
51
53
|
this.matchParentBlock = this.curBlock;
|
|
52
54
|
this.inMatchPattern = true;
|
|
@@ -69,6 +69,13 @@ function handleUnaryPositive() {
|
|
|
69
69
|
|
|
70
70
|
function handleToBool() {
|
|
71
71
|
// Python 3.13+ TO_BOOL: normalize truthiness. Preserve stack value.
|
|
72
|
+
// The preceding LOAD+COPY 1 here is the boolean-shortcut idiom for
|
|
73
|
+
// `if not X` / `while X`, not a match-case subject. Clear the
|
|
74
|
+
// potential-match candidate so a later COMPARE_OP (e.g. `e.code == 409`
|
|
75
|
+
// inside an except handler) doesn't get retroactively wrapped in a
|
|
76
|
+
// synthetic `match X:` rooted on this stale subject.
|
|
77
|
+
this.potentialMatchSubject = null;
|
|
78
|
+
this.matchCandidateStart = -1;
|
|
72
79
|
const arg = this.dataStack.pop();
|
|
73
80
|
this.dataStack.push(arg);
|
|
74
81
|
}
|