depyo 1.0.2 → 1.0.3

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.
@@ -1,5 +1,30 @@
1
1
  const AST = require('../ast/ast_node');
2
2
 
3
+ // CPython Include/internal/pycore_intrinsics.h
4
+ const INTRINSIC_1 = Object.freeze({
5
+ INVALID: 0,
6
+ PRINT: 1,
7
+ IMPORT_STAR: 2,
8
+ STOPITERATION_ERROR: 3,
9
+ ASYNC_GEN_WRAP: 4,
10
+ UNARY_POSITIVE: 5,
11
+ LIST_TO_TUPLE: 6,
12
+ TYPEVAR: 7,
13
+ PARAMSPEC: 8,
14
+ TYPEVARTUPLE: 9,
15
+ SUBSCRIPT_GENERIC: 10,
16
+ TYPEALIAS: 11,
17
+ });
18
+
19
+ const INTRINSIC_2 = Object.freeze({
20
+ INVALID: 0,
21
+ PREP_RERAISE_STAR: 1,
22
+ TYPEVAR_WITH_BOUND: 2,
23
+ TYPEVAR_WITH_CONSTRAINTS: 3,
24
+ SET_FUNCTION_TYPE_PARAMS: 4,
25
+ SET_TYPEPARAM_DEFAULT: 5, // 3.13+
26
+ });
27
+
3
28
  function handleKwNamesA() {
4
29
  let astNode = new AST.ASTKwNamesMap();
5
30
  let keys = this.code.Current.ConstantObject;
@@ -20,8 +45,55 @@ function handleCallFunctionA() {
20
45
  }
21
46
 
22
47
  function handleInstrumentedCallKwA() {
23
- // Instrumented CALL_KW behaves like CALL with kw count encoded in argument.
24
- handleInstrumentedCallA.call(this);
48
+ // Instrumented CALL_KW behaves like CALL_KW.
49
+ handleCallKwA.call(this);
50
+ }
51
+
52
+ function handleCallKwA() {
53
+ // Python 3.13 CALL_KW: (callable, self_or_null, args[oparg], kwnames -- res)
54
+ // kwnames is a tuple of strings; the last len(kwnames) of args[] are the kw values.
55
+ let kwnamesNode = this.dataStack.pop();
56
+ let kwNamesList = [];
57
+ const toKwName = (v) => {
58
+ // Render kwarg keys as bare identifiers (foo=1), not string literals ("foo"=1).
59
+ const raw = v?.Value ?? v?.name ?? v;
60
+ const name = typeof raw === 'string' ? raw : String(raw);
61
+ return new AST.ASTName(name.replace(/^['"]|['"]$/g, ''));
62
+ };
63
+ if (kwnamesNode instanceof AST.ASTObject) {
64
+ const obj = kwnamesNode.object;
65
+ if (obj && (obj.ClassName === 'Py_Tuple' || obj.ClassName === 'Py_SmallTuple') && Array.isArray(obj.Value)) {
66
+ kwNamesList = obj.Value.map(toKwName);
67
+ }
68
+ } else if (kwnamesNode instanceof AST.ASTTuple) {
69
+ kwNamesList = (kwnamesNode.values || []).map(toKwName);
70
+ }
71
+
72
+ const totalArgs = this.code.Current.Argument;
73
+ const kwcount = kwNamesList.length;
74
+ const pcount = Math.max(0, totalArgs - kwcount);
75
+
76
+ let kwparamList = [];
77
+ for (let i = kwcount - 1; i >= 0; i--) {
78
+ let value = this.dataStack.pop();
79
+ kwparamList.unshift({ key: kwNamesList[i], value });
80
+ }
81
+
82
+ let pparamList = [];
83
+ for (let i = 0; i < pcount; i++) {
84
+ pparamList.unshift(this.dataStack.pop());
85
+ }
86
+
87
+ let func = this.dataStack.pop();
88
+ if (func === null && this.dataStack.length > 0) {
89
+ func = this.dataStack.pop();
90
+ } else if (this.dataStack.length > 0 && this.dataStack.top() == null) {
91
+ this.dataStack.pop();
92
+ }
93
+
94
+ let callNode = new AST.ASTCall(func, pparamList, kwparamList);
95
+ callNode.line = this.code.Current.LineNo;
96
+ this.dataStack.push(callNode);
25
97
  }
26
98
 
27
99
  function handleEnterExecutorA() {
@@ -36,34 +108,66 @@ function handleInstrumentedCallA() {
36
108
  let pparams = (this.code.Current.Argument & 0xFF);
37
109
  let kwparamList = [];
38
110
  let pparamList = [];
39
- let loadBuildClassFound = false;
40
111
 
41
- for (let idx = this.dataStack.length - 1; idx >= 0; idx--) {
42
- if (this.dataStack[idx] instanceof AST.ASTLoadBuildClass) {
43
- loadBuildClassFound = true;
44
- break;
45
- }
112
+ // This CALL invokes __build_class__ iff a LoadBuildClass marker sits on the
113
+ // stack at exactly the depth this CALL will consume down to. Otherwise a
114
+ // nested CALL (e.g. `type(Y)` in `class X(type(Y)):`) will falsely trigger
115
+ // the build-class branch because LoadBuildClass is still somewhere below.
116
+ //
117
+ // Stack layout (bottom → top) varies by version:
118
+ // pre-3.6 CALL_FUNCTION: [LBC, fn, name, ...bases, k1,v1, ...]; LBC at pparams + 2*kwparams
119
+ // 3.6+ CALL_FUNCTION: [LBC, fn, name, ...bases]; LBC at pparams
120
+ // 3.11/3.12 CALL: [NULL, LBC, fn, name, ...bases]; LBC at pparams
121
+ // (PUSH_NULL emitted before LOAD_BUILD_CLASS, NULL below LBC)
122
+ // 3.13+ CALL: [LBC, NULL, fn, name, ...bases]; LBC at pparams + 1
123
+ // (LOAD_BUILD_CLASS emitted before PUSH_NULL, NULL above LBC; peel in pop loop)
124
+ const stackHas = (d) => {
125
+ const idx = this.dataStack.length - 1 - d;
126
+ return idx >= 0 && this.dataStack[idx] instanceof AST.ASTLoadBuildClass;
127
+ };
128
+ let loadBuildClassFound = false;
129
+ if (this.object.Reader.versionCompare(3, 6) >= 0) {
130
+ loadBuildClassFound = stackHas(pparams) || stackHas(pparams + 1);
131
+ } else {
132
+ loadBuildClassFound = stackHas(pparams + 2 * kwparams);
46
133
  }
47
134
 
48
135
  if (loadBuildClassFound) {
49
136
  let bases = [];
50
- let TOS = this.dataStack.top();
51
-
52
- while (TOS instanceof AST.ASTName || TOS instanceof AST.ASTBinary) {
53
- bases.push(TOS);
54
- this.dataStack.pop();
55
- TOS = this.dataStack.top();
137
+ let lbcKwparamList = [];
138
+ const nbases = Math.max(0, pparams - 2);
139
+
140
+ if (this.object.Reader.versionCompare(3, 6) < 0) {
141
+ // Pre-3.6: kwargs are alternating key/value pairs on stack.
142
+ for (let i = 0; i < kwparams; i++) {
143
+ let value = this.dataStack.pop();
144
+ let key = this.dataStack.pop();
145
+ lbcKwparamList.unshift({key, value});
146
+ }
147
+ for (let i = 0; i < nbases; i++) {
148
+ bases.unshift(this.dataStack.pop());
149
+ }
150
+ } else {
151
+ // 3.6+: pop exactly nbases items; kwargs flow via CALL_FUNCTION_KW/KW_NAMES.
152
+ // Bases can be arbitrary expressions (ASTName, ASTBinary, ASTCall, ASTSubscr, …).
153
+ for (let i = 0; i < nbases; i++) {
154
+ bases.unshift(this.dataStack.pop());
155
+ }
56
156
  }
57
157
 
58
- // qualified name is PycString at TOS
59
158
  let name = this.dataStack.pop();
60
159
  let functionNode = this.dataStack.pop();
61
160
  let loadbuild = this.dataStack.pop();
161
+ // 3.11+: PUSH_NULL sits between __build_class__ and the function; peel it off.
162
+ if (loadbuild === null && this.dataStack.top() instanceof AST.ASTLoadBuildClass) {
163
+ loadbuild = this.dataStack.pop();
164
+ }
62
165
  if (loadbuild instanceof AST.ASTLoadBuildClass) {
63
- let callNode = new AST.ASTCall(functionNode, pparamList, kwparamList);
166
+ let callNode = new AST.ASTCall(functionNode, [], lbcKwparamList);
64
167
  callNode.line = this.code.Current.LineNo;
65
168
  let classNode = new AST.ASTClass(callNode, new AST.ASTTuple(bases), name);
66
169
  classNode.line = this.code.Current.LineNo;
170
+ classNode.kwargs = lbcKwparamList;
67
171
  this.dataStack.push(classNode);
68
172
  return;
69
173
  }
@@ -104,6 +208,15 @@ function handleInstrumentedCallA() {
104
208
  skipCallNode = true;
105
209
  break;
106
210
  }
211
+ } else if (param instanceof AST.ASTClass && pparams == 1 && kwparamList.length === 0) {
212
+ // Class decorator: decorator(class) — attach decorator and re-push class
213
+ // so chained class decorators (e.g. @author("Me")\n@author("You")) unwind
214
+ // naturally on the data stack.
215
+ let decorator = this.dataStack.pop();
216
+ param.add_decorator(decorator);
217
+ this.dataStack.push(param);
218
+ skipCallNode = true;
219
+ break;
107
220
  } else {
108
221
  pparamList.unshift(param);
109
222
  }
@@ -114,38 +227,68 @@ function handleInstrumentedCallA() {
114
227
  }
115
228
 
116
229
  let func = this.dataStack.pop();
117
- // 3.11+ PUSH_NULL precedes callable; if func is null, grab the real callable underneath.
230
+ // 3.11+ CALL convention: stack is [callable, self_or_null, args...].
231
+ // We just popped the args; `func` is actually self_or_null, and the real callable is underneath.
118
232
  if ([this.OpCodes.CALL_A, this.OpCodes.INSTRUMENTED_CALL_A].includes(this.code.Current.OpCodeID)) {
119
233
  if (func === null && this.dataStack.length > 0) {
234
+ // Normal call: PUSH_NULL was self_or_null, real callable below.
120
235
  func = this.dataStack.pop();
121
236
  } else if (this.dataStack.length > 0 && this.dataStack.top() == null) {
237
+ // Edge case: null sits one slot deeper (rare ordering). Drop it.
122
238
  this.dataStack.pop();
123
- }
124
- // Python 3.14 LOAD_SPECIAL pushes [self, method] for special method calls.
125
- // If func is an attr access for __enter__/__exit__/__aenter__/__aexit__,
126
- // pop the self since it's consumed by the unbound method call.
127
- else if (func instanceof AST.ASTBinary && func.op === AST.ASTBinary.BinOp.Attr) {
239
+ } else if (func instanceof AST.ASTBinary && func.op === AST.ASTBinary.BinOp.Attr) {
240
+ // Python 3.14 LOAD_SPECIAL pushes [self, method] for __enter__/__exit__ etc.;
241
+ // the self below is consumed by the unbound special-method call.
128
242
  const attrName = func.right?.name;
129
243
  if (['__enter__', '__exit__', '__aenter__', '__aexit__'].includes(attrName)) {
130
244
  if (this.dataStack.length > 0) {
131
- this.dataStack.pop(); // Pop self consumed by special method
245
+ this.dataStack.pop();
246
+ }
247
+ }
248
+ } else if (this.dataStack.length > 0) {
249
+ // Bound method / decorator pattern: self_or_null is non-null, so
250
+ // the call is real_callable(func, *args). Pop the real callable and
251
+ // promote `func` to the first positional arg.
252
+ const below = this.dataStack.top();
253
+ if (below !== null && below !== undefined && !(below instanceof AST.ASTLoadBuildClass)) {
254
+ const realCallable = this.dataStack.pop();
255
+ // Decorator syntax: single ASTFunction arg with a real name → attach decorator,
256
+ // push the decorated function back so the next STORE renders as `@decorator\ndef name(...)`.
257
+ if (pparamList.length === 0 &&
258
+ func instanceof AST.ASTFunction &&
259
+ func.code?.object?.Name &&
260
+ func.code.object.Name !== "<lambda>") {
261
+ func.add_decorator(realCallable);
262
+ this.dataStack.push(func);
263
+ return;
132
264
  }
265
+ pparamList.unshift(func);
266
+ func = realCallable;
133
267
  }
134
268
  }
135
269
  }
136
270
 
137
271
  if (func instanceof AST.ASTFunction) {
138
- const compNames = new Set(["<listcomp>", "<setcomp>", "<dictcomp>", "<genexpr>"]);
272
+ const compNames = new Set(["<listcomp>", "<setcomp>", "<dictcomp>", "<genexpr>", "<generator expression>"]);
139
273
  const codeObj = func.code?.object;
140
274
  if (codeObj && !codeObj.SourceCode) {
275
+ const PycDecompiler = require('../PycDecompiler');
276
+ const innerDecompiler = new PycDecompiler(codeObj);
141
277
  try {
142
- const PycDecompiler = require('../PycDecompiler');
143
- codeObj.SourceCode = new PycDecompiler(codeObj).decompile();
278
+ codeObj.SourceCode = innerDecompiler.decompile();
144
279
  } catch (e) {
280
+ if (global.g_cliArgs?.strict) throw e;
281
+ this.errors.push({
282
+ opcode: 'NESTED_DECOMPILE',
283
+ codeObject: codeObj?.Name?.toString?.() || '<unknown>',
284
+ message: e?.message || String(e),
285
+ stack: e?.stack
286
+ });
145
287
  if (global.g_cliArgs?.debug) {
146
288
  console.error(`[CALL] Failed to decompile nested function: ${e?.message}`);
147
289
  }
148
290
  }
291
+ if (innerDecompiler.errors.length) this.errors.push(...innerDecompiler.errors);
149
292
  }
150
293
 
151
294
  const sourceTop = codeObj?.SourceCode?.list?.top?.() || null;
@@ -164,6 +307,84 @@ function handleInstrumentedCallA() {
164
307
  resultNode = resultNode.value;
165
308
  }
166
309
 
310
+ // Async comprehension: inner code uses GET_ANEXT instead of
311
+ // FOR_ITER, so no ASTComprehension was produced. Reconstruct
312
+ // from the decompiled source tree.
313
+ if (!(resultNode instanceof AST.ASTComprehension) && codeObj?.SourceCode) {
314
+ let yieldExpr = null;
315
+ let loopVar = null;
316
+ const searchNodes = (nodes) => {
317
+ if (!nodes) return;
318
+ for (const node of nodes) {
319
+ if (node instanceof AST.ASTReturn && node.rettype === AST.ASTReturn.RetType.Yield && node.value) {
320
+ yieldExpr = node.value;
321
+ }
322
+ if (node instanceof AST.ASTStore && !loopVar) {
323
+ loopVar = node.dest;
324
+ }
325
+ if (node instanceof AST.ASTIterBlock && node.blockType === AST.ASTBlock.BlockType.AsyncFor) {
326
+ if (node.index) loopVar = node.index;
327
+ if (node.nodes) searchNodes(node.nodes);
328
+ }
329
+ if (node?.nodes) searchNodes(node.nodes);
330
+ }
331
+ };
332
+ searchNodes(codeObj.SourceCode.list);
333
+ // Listcomp/setcomp/dictcomp fallback: the yield expression was
334
+ // saved by processListAppend/MAP_ADD when no For+comp block
335
+ // existed.
336
+ if (!yieldExpr && codeObj._asyncCompYieldExpr) {
337
+ yieldExpr = codeObj._asyncCompYieldExpr;
338
+ }
339
+ // In 3.8+ inline async comprehensions the STORE_FAST lands
340
+ // inside a try/except block and disappears into the block
341
+ // structure before reaching SourceCode.list. Scan the inner
342
+ // code's instruction list: first STORE_* after GET_ANEXT is
343
+ // the loop variable.
344
+ if (!loopVar) {
345
+ const innerOps = new this.OpCodes(codeObj);
346
+ let sawAnext = false;
347
+ for (const op of innerOps.Instructions || []) {
348
+ if (!op) continue;
349
+ if (op.OpCodeID === this.OpCodes.GET_ANEXT) {
350
+ sawAnext = true;
351
+ continue;
352
+ }
353
+ if (sawAnext && (op.OpCodeID === this.OpCodes.STORE_FAST_A ||
354
+ op.OpCodeID === this.OpCodes.STORE_NAME_A ||
355
+ op.OpCodeID === this.OpCodes.STORE_DEREF_A)) {
356
+ const varName = op.Name?.toString?.() || '';
357
+ if (varName) {
358
+ loopVar = new AST.ASTName(varName);
359
+ break;
360
+ }
361
+ }
362
+ }
363
+ }
364
+ if (global.g_cliArgs?.debug) {
365
+ console.log(`[CALL-RECONSTRUCT] ${codeObj.Name?.toString?.()} yield=${yieldExpr?.constructor?.name} loopVar=${loopVar?.name}`);
366
+ }
367
+ if (yieldExpr && loopVar) {
368
+ let kind = AST.ASTComprehension.GENERATOR;
369
+ const objName = codeObj.Name?.toString?.() || '';
370
+ if (objName.includes('listcomp')) kind = AST.ASTComprehension.LIST;
371
+ else if (objName.includes('setcomp')) kind = AST.ASTComprehension.SET;
372
+ else if (objName.includes('dictcomp')) kind = AST.ASTComprehension.DICT;
373
+ let comp;
374
+ if (kind === AST.ASTComprehension.DICT && codeObj._asyncCompYieldKey) {
375
+ comp = new AST.ASTComprehension(yieldExpr, codeObj._asyncCompYieldKey);
376
+ } else {
377
+ comp = new AST.ASTComprehension(yieldExpr);
378
+ }
379
+ comp.kind = kind;
380
+ let gen = new AST.ASTIterBlock(AST.ASTBlock.BlockType.AsyncFor, 0, 0, new AST.ASTName('.0'));
381
+ gen.index = loopVar;
382
+ gen.comprehension = true;
383
+ comp.addGenerator(gen);
384
+ resultNode = comp;
385
+ }
386
+ }
387
+
167
388
  // Map placeholder iter (.0) to actual argument for comprehensions.
168
389
  if (resultNode?.generators) {
169
390
  if (global.g_cliArgs?.debug) {
@@ -177,18 +398,40 @@ function handleInstrumentedCallA() {
177
398
  if (global.g_cliArgs?.debug) {
178
399
  console.log(`[CALL] remapped iter .${paramIdx} -> ${gen.iter?.constructor?.name}`);
179
400
  }
401
+ } else if (gen.iter instanceof AST.ASTName && gen.iter.name === "[outmost-iterable]") {
402
+ // Py2.4 names the genexpr's first parameter `[outmost-iterable]`
403
+ const param = pparamList[0];
404
+ gen.iter = param instanceof AST.ASTIteratorValue ? param.value : param;
180
405
  }
181
406
  }
182
407
  }
183
408
 
184
409
  if (resultNode) {
410
+ // Async listcomp/setcomp/dictcomp: the outer function
411
+ // awaits the comprehension coroutine with GET_AWAITABLE +
412
+ // LOAD_CONST None + YIELD_FROM. Skip these — the await is
413
+ // implicit in the async comprehension syntax.
414
+ if (resultNode instanceof AST.ASTComprehension &&
415
+ resultNode.kind !== AST.ASTComprehension.GENERATOR) {
416
+ const nx1 = this.code.Next;
417
+ const nx2 = nx1?.Next;
418
+ const nx3 = nx2?.Next;
419
+ if (nx1 && nx2 && nx3 &&
420
+ (nx1.OpCodeID == this.OpCodes.GET_AWAITABLE ||
421
+ nx1.OpCodeID == this.OpCodes.GET_AWAITABLE_A) &&
422
+ nx2.OpCodeID == this.OpCodes.LOAD_CONST_A &&
423
+ nx3.OpCodeID == this.OpCodes.YIELD_FROM) {
424
+ this.code.GoNext(3);
425
+ }
426
+ }
185
427
  this.dataStack.push(resultNode);
186
428
  return;
187
429
  }
188
430
  }
189
431
  }
190
432
 
191
- if ([this.OpCodes.GET_ITER, this.OpCodes.GET_AITER].includes(this.code.Prev.OpCodeID)) {
433
+ if ([this.OpCodes.GET_ITER, this.OpCodes.GET_AITER].includes(this.code.Prev.OpCodeID) &&
434
+ func instanceof AST.ASTFunction && func.code?.object?.SourceCode?.list?.top) {
192
435
  let ast = func.code.object.SourceCode.list.top();
193
436
  if (!(ast instanceof AST.ASTKeyword)) {
194
437
  if (ast instanceof AST.ASTReturn) {
@@ -203,6 +446,12 @@ function handleInstrumentedCallA() {
203
446
  param = param.value;
204
447
  }
205
448
  gen.iter = param;
449
+ } else if (gen.iter instanceof AST.ASTName && gen.iter.name === "[outmost-iterable]") {
450
+ let param = pparamList[0];
451
+ if (param instanceof AST.ASTIteratorValue) {
452
+ param = param.value;
453
+ }
454
+ gen.iter = param;
206
455
  }
207
456
  }
208
457
  }
@@ -250,6 +499,66 @@ function handleCallFunctionVarA() {
250
499
  }
251
500
 
252
501
  function handleCallFunctionKwA() {
502
+ if (this.object.Reader.versionCompare(3, 6) >= 0) {
503
+ // Py 3.6+: CALL_FUNCTION_KW(argc) — TOS is a tuple of kwarg names,
504
+ // argc is total args, last len(names) are kwargs.
505
+ let kwnamesNode = this.dataStack.pop();
506
+ let kwNamesList = [];
507
+ const toKwName = (v) => {
508
+ const raw = v?.Value ?? v?.name ?? v;
509
+ const name = typeof raw === 'string' ? raw : String(raw);
510
+ return new AST.ASTName(name.replace(/^['"]|['"]$/g, ''));
511
+ };
512
+ if (kwnamesNode instanceof AST.ASTObject) {
513
+ const obj = kwnamesNode.object;
514
+ if (obj && (obj.ClassName === 'Py_Tuple' || obj.ClassName === 'Py_SmallTuple') && Array.isArray(obj.Value)) {
515
+ kwNamesList = obj.Value.map(toKwName);
516
+ }
517
+ } else if (kwnamesNode instanceof AST.ASTTuple) {
518
+ kwNamesList = (kwnamesNode.values || []).map(toKwName);
519
+ }
520
+
521
+ const totalArgs = this.code.Current.Argument;
522
+ const kwcount = kwNamesList.length;
523
+ const pcount = Math.max(0, totalArgs - kwcount);
524
+
525
+ let kwparamList = [];
526
+ for (let i = kwcount - 1; i >= 0; i--) {
527
+ let value = this.dataStack.pop();
528
+ kwparamList.unshift({ key: kwNamesList[i], value });
529
+ }
530
+
531
+ let pparamList = [];
532
+ for (let i = 0; i < pcount; i++) {
533
+ pparamList.unshift(this.dataStack.pop());
534
+ }
535
+
536
+ let func = this.dataStack.pop();
537
+
538
+ // 3.6+ class with kwargs uses CALL_FUNCTION_KW:
539
+ // LOAD_BUILD_CLASS / MAKE_FUNCTION / LOAD_CONST name / <bases>
540
+ // / LOAD_CONST (kwarg_names_tuple) / CALL_FUNCTION_KW argc
541
+ // pparamList is [body_func, class_name, ...bases]; kwparamList carries
542
+ // the class kwargs (metaclass, **__init_subclass__ kwargs, ...).
543
+ if (func instanceof AST.ASTLoadBuildClass && pparamList.length >= 2) {
544
+ const functionNode = pparamList[0];
545
+ const nameNode = pparamList[1];
546
+ const bases = pparamList.slice(2);
547
+ const classCall = new AST.ASTCall(functionNode, [], kwparamList);
548
+ classCall.line = this.code.Current.LineNo;
549
+ const classNode = new AST.ASTClass(classCall, new AST.ASTTuple(bases), nameNode);
550
+ classNode.line = this.code.Current.LineNo;
551
+ classNode.kwargs = kwparamList;
552
+ this.dataStack.push(classNode);
553
+ return;
554
+ }
555
+
556
+ let callNode = new AST.ASTCall(func, pparamList, kwparamList);
557
+ callNode.line = this.code.Current.LineNo;
558
+ this.dataStack.push(callNode);
559
+ return;
560
+ }
561
+
253
562
  let kw = this.dataStack.pop();
254
563
  let kwparams = (this.code.Current.Argument & 0xFF00) >> 8;
255
564
  let pparams = (this.code.Current.Argument & 0xFF);
@@ -339,34 +648,67 @@ function handleBinaryCall() {
339
648
  this.dataStack.push(callNode);
340
649
  }
341
650
 
651
+ function mapKeysAreIdentifiers(mapNode) {
652
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
653
+ for (const entry of mapNode.values || []) {
654
+ const keyObj = entry?.key;
655
+ const cls = keyObj?.object?.ClassName;
656
+ if (cls !== 'Py_String' && cls !== 'Py_Unicode') {
657
+ return false;
658
+ }
659
+ const s = keyObj.object.toString();
660
+ if (!IDENT.test(s)) {
661
+ return false;
662
+ }
663
+ }
664
+ return true;
665
+ }
666
+
342
667
  function handleCallFunctionExA() {
668
+ // CPython 3.6+: oparg & 0x01 means kwargs mapping is on TOS; the positional
669
+ // tuple and the callable are ALWAYS popped regardless of the flag.
343
670
  let flags = this.code.Current.Argument;
344
671
  let kwparams = [];
345
672
  let pparams = [];
673
+ let varArg = null;
674
+ let kwSpread = null;
675
+ let kwSingle = null;
346
676
  if (flags & 0x01) { // **kwargs
347
677
  let kw = this.dataStack.pop();
348
- if (kw instanceof AST.ASTMap) {
349
- kwparams = kw.values;
678
+ if (kw instanceof AST.ASTMapUnpack) {
679
+ // Py 3.6+ f(**a, **b) path: preserve individual ** sources for rendering.
680
+ kwSpread = kw;
681
+ } else if (kw instanceof AST.ASTMap) {
682
+ if (mapKeysAreIdentifiers(kw)) {
683
+ kwparams = kw.values;
684
+ } else {
685
+ // Non-identifier keys (e.g. '"', 'with space') must render as **{...}.
686
+ kwSingle = kw;
687
+ }
350
688
  } else if (kw?.object?.ClassName === "Py_Dict" && kw.object.Value) {
351
689
  kwparams = kw.object.Value.map(entry => ({key: new AST.ASTObject(entry.key), value: new AST.ASTObject(entry.value)}));
352
690
  } else {
353
- if (global.g_cliArgs?.debug) {
354
- console.error("Expected a map for CALL_FUNCTION_EX kwargs");
355
- }
691
+ // Single **dict argument where dict is a name/expression (not literal).
692
+ kwSingle = kw;
356
693
  }
357
694
  }
358
- if (flags & 0x02) { // *args
359
- let args = this.dataStack.pop();
360
- if (args instanceof AST.ASTTuple || args instanceof AST.ASTList) {
361
- pparams = args.values;
362
- } else {
363
- if (global.g_cliArgs?.debug) {
364
- console.error("Expected a tuple or list for CALL_FUNCTION_EX args");
365
- }
366
- }
695
+ let args = this.dataStack.pop();
696
+ if (args instanceof AST.ASTTuple || args instanceof AST.ASTList) {
697
+ pparams = args.values;
698
+ } else {
699
+ // *args as a non-literal expression (e.g. f(*tup_name, **kw)).
700
+ varArg = args;
367
701
  }
368
702
  let func = this.dataStack.pop();
369
703
  let callNode = new AST.ASTCall(func, pparams, kwparams);
704
+ if (varArg) {
705
+ callNode.var = varArg;
706
+ }
707
+ if (kwSpread) {
708
+ callNode.kw = kwSpread;
709
+ } else if (kwSingle) {
710
+ callNode.kw = kwSingle;
711
+ }
370
712
  callNode.line = this.code.Current.LineNo;
371
713
  this.dataStack.push(callNode);
372
714
  }
@@ -374,19 +716,15 @@ function handleCallFunctionExA() {
374
716
  function handleCallIntrinsic1() {
375
717
  // Python 3.12+ CALL_INTRINSIC_1 opcode
376
718
  // Calls built-in intrinsic functions (argument specifies which)
377
- // Intrinsic 3: ASYNC_GEN_WRAP (wraps async generator function)
378
- // Intrinsic 4: STOPITERATION_ERROR, etc.
379
719
 
380
- // Intrinsic takes 1 arg from stack, returns result
381
720
  let arg = this.dataStack.pop();
382
721
 
383
722
  if (global.g_cliArgs?.debug) {
384
723
  console.log(`[CALL_INTRINSIC_1] intrinsic=${this.code.Current.Argument}, arg=${arg?.constructor?.name}`);
385
724
  }
386
725
 
387
- // Intrinsic 11 (PEP 695 type statement) wraps a tuple(name, qualname, type-fn)
388
- const TYPE_ALIAS_INTRINSIC = 11;
389
- if (this.code.Current.Argument === TYPE_ALIAS_INTRINSIC && arg instanceof AST.ASTTuple) {
726
+ // PEP 695 type statement wraps tuple(name, qualname, type-fn)
727
+ if (this.code.Current.Argument === INTRINSIC_1.TYPEALIAS && arg instanceof AST.ASTTuple) {
390
728
  const values = arg.values || [];
391
729
  const aliasName = values[0]?.object?.Value || values[0]?.name || values[0]?.codeFragment?.();
392
730
  const typeFunc = values[2];
@@ -432,9 +770,9 @@ function handleCallIntrinsic2() {
432
770
  const argA = this.dataStack.pop();
433
771
  const args = [argA, argB];
434
772
 
435
- // Intrinsic 1: PREP_RERAISE_STAR equivalent in 3.12+.
773
+ // PREP_RERAISE_STAR equivalent in 3.12+.
436
774
  // Do not emit; mark to skip the following conditional jump.
437
- if (this.code.Current.Argument === 1) {
775
+ if (this.code.Current.Argument === INTRINSIC_2.PREP_RERAISE_STAR) {
438
776
  this.ignoreNextConditional = true;
439
777
  this.cleanupStackDepth = this.dataStack.length + 1; // after we push a placeholder
440
778
  this.dataStack.push(new AST.ASTNone());
@@ -442,23 +780,81 @@ function handleCallIntrinsic2() {
442
780
  return;
443
781
  }
444
782
 
445
- // Intrinsic 4: build generic function from type parameters + function
446
- if (this.code.Current.Argument === 4) {
783
+ // SET_FUNCTION_TYPE_PARAMS build generic function from
784
+ // type parameters tuple + function
785
+ if (this.code.Current.Argument === INTRINSIC_2.SET_FUNCTION_TYPE_PARAMS) {
447
786
  const funcNode = args.find(a => a instanceof AST.ASTFunction);
448
787
  const typeArg = args.find(a => a instanceof AST.ASTTuple || a instanceof AST.ASTList);
449
788
  if (funcNode && typeArg) {
450
- const names = (typeArg.values || []).map(v => {
451
- if (v instanceof AST.ASTName) return v.name;
452
- if (v?.object?.Value) return v.object.Value;
453
- const frag = v?.codeFragment?.();
454
- return frag?.toString?.() || 'T';
789
+ const params = (typeArg.values || []).map(v => {
790
+ // COPY/STORE_FAST inside the wrapper wraps each typevar in a
791
+ // walrus ASTNamedExpr(name := typevar). Unwrap to reach the
792
+ // underlying typevar (which carries pepDefault for PEP 696).
793
+ let source = v;
794
+ if (v instanceof AST.ASTNamedExpr) {
795
+ source = v.value;
796
+ }
797
+ let name;
798
+ if (v instanceof AST.ASTNamedExpr && v.target?.name) {
799
+ name = v.target.name;
800
+ } else if (source instanceof AST.ASTName) {
801
+ name = source.name;
802
+ } else if (source?.object?.Value) {
803
+ name = source.object.Value;
804
+ } else {
805
+ const frag = source?.codeFragment?.();
806
+ name = frag?.toString?.() || 'T';
807
+ }
808
+ if (source?.pepDefault != null) {
809
+ return { name, default: source.pepDefault };
810
+ }
811
+ return name;
455
812
  });
456
- funcNode.typeParams = names;
813
+ funcNode.typeParams = params;
457
814
  this.dataStack.push(funcNode);
458
815
  return;
459
816
  }
460
817
  }
461
818
 
819
+ // SET_TYPEPARAM_DEFAULT (PEP 696, 3.13+)
820
+ // Stack: [..., typevar, default_fn] → [..., typevar_with_default]
821
+ // The default wrapper is a zero-arg function whose return value is the default.
822
+ if (this.code.Current.Argument === INTRINSIC_2.SET_TYPEPARAM_DEFAULT) {
823
+ const defaultFn = argB;
824
+ const typevar = argA;
825
+ let defaultValue = null;
826
+ const extractReturn = (body) => {
827
+ const list = body?.list || [];
828
+ for (let i = list.length - 1; i >= 0; i--) {
829
+ if (list[i] instanceof AST.ASTReturn && list[i].value) {
830
+ return list[i].value;
831
+ }
832
+ }
833
+ return null;
834
+ };
835
+ if (defaultFn instanceof AST.ASTFunction) {
836
+ defaultValue = extractReturn(defaultFn.code?.object?.SourceCode);
837
+ } else if (defaultFn instanceof AST.ASTObject &&
838
+ (defaultFn.object?.ClassName === 'Py_CodeObject' ||
839
+ defaultFn.object?.ClassName === 'Py_CodeObject2')) {
840
+ try {
841
+ const PycDecompiler = require('../PycDecompiler');
842
+ const dd = new PycDecompiler(defaultFn.object);
843
+ const body = dd.decompile();
844
+ defaultFn.object.SourceCode = body;
845
+ if (dd.errors.length) this.errors.push(...dd.errors);
846
+ defaultValue = extractReturn(body);
847
+ } catch (_) { /* bare typevar */ }
848
+ }
849
+ if (typevar != null && defaultValue != null) {
850
+ typevar.pepDefault = defaultValue;
851
+ }
852
+ if (typevar !== undefined) {
853
+ this.dataStack.push(typevar);
854
+ }
855
+ return;
856
+ }
857
+
462
858
  // Default: preserve the left argument to avoid stack underflow
463
859
  if (argA !== undefined) {
464
860
  this.dataStack.push(argA);
@@ -478,6 +874,7 @@ function handleCallIntrinsic2A() {
478
874
  module.exports = {
479
875
  handleKwNamesA,
480
876
  handleCallA,
877
+ handleCallKwA,
481
878
  handleInstrumentedCallKwA,
482
879
  handleCallFunctionA,
483
880
  handleInstrumentedCallA,