depyo 1.0.2 → 1.1.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.
@@ -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;
@@ -154,7 +297,10 @@ function handleInstrumentedCallA() {
154
297
  const topType = sourceTop?.constructor?.name || 'null';
155
298
  console.log(`[CALL] func=${codeObj?.Name || codeObj?.QualName?.Value || '<?>'} sourceTop=${topType} compNode=${compNode?.constructor?.name || 'null'} hasSource=${!!codeObj?.SourceCode} pparams=${pparamList.length}`);
156
299
  }
157
- const looksLikeComp = compNames.has(func.code?.object?.Name) ||
300
+ // Name is a PythonObject (Py_String/Py_Unicode), not a raw string — compare
301
+ // via toString() so the Set lookup actually matches.
302
+ const codeName = func.code?.object?.Name?.toString?.() || '';
303
+ const looksLikeComp = compNames.has(codeName) ||
158
304
  sourceTop instanceof AST.ASTComprehension ||
159
305
  compNode instanceof AST.ASTComprehension ||
160
306
  (sourceTop instanceof AST.ASTReturn && sourceTop.value instanceof AST.ASTComprehension);
@@ -164,6 +310,100 @@ function handleInstrumentedCallA() {
164
310
  resultNode = resultNode.value;
165
311
  }
166
312
 
313
+ // Async comprehension: inner code uses GET_ANEXT instead of
314
+ // FOR_ITER, so no ASTComprehension was produced. Reconstruct
315
+ // from the decompiled source tree.
316
+ if (!(resultNode instanceof AST.ASTComprehension) && codeObj?.SourceCode) {
317
+ let yieldExpr = null;
318
+ let loopVar = null;
319
+ // Seed async-ness from the code flags: async comprehensions and
320
+ // async generator expressions carry CO_ASYNC_GENERATOR /
321
+ // CO_COROUTINE on the inner code object, so use that as the
322
+ // default in case the inner tree lacks an explicit AsyncFor
323
+ // block (e.g. pre-3.8 async comprehensions use GET_ANEXT +
324
+ // STORE instead of a FOR_ITER-driven iter block).
325
+ const flags = codeObj?.Flags | 0;
326
+ let isAsyncLoop = !!(flags & AST.ASTFunction.CodeFlags.CO_ASYNC_GENERATOR) ||
327
+ !!(flags & AST.ASTFunction.CodeFlags.CO_COROUTINE);
328
+ const searchNodes = (nodes) => {
329
+ if (!nodes) return;
330
+ for (const node of nodes) {
331
+ if (node instanceof AST.ASTReturn && node.rettype === AST.ASTReturn.RetType.Yield && node.value) {
332
+ yieldExpr = node.value;
333
+ }
334
+ if (node instanceof AST.ASTStore && !loopVar) {
335
+ loopVar = node.dest;
336
+ }
337
+ if (node instanceof AST.ASTIterBlock &&
338
+ (node.blockType === AST.ASTBlock.BlockType.AsyncFor ||
339
+ node.blockType === AST.ASTBlock.BlockType.For)) {
340
+ if (node.index) {
341
+ loopVar = node.index;
342
+ isAsyncLoop = (node.blockType === AST.ASTBlock.BlockType.AsyncFor);
343
+ }
344
+ if (node.nodes) searchNodes(node.nodes);
345
+ }
346
+ if (node?.nodes) searchNodes(node.nodes);
347
+ }
348
+ };
349
+ searchNodes(codeObj.SourceCode.list);
350
+ // Listcomp/setcomp/dictcomp fallback: the yield expression was
351
+ // saved by processListAppend/MAP_ADD when no For+comp block
352
+ // existed.
353
+ if (!yieldExpr && codeObj._asyncCompYieldExpr) {
354
+ yieldExpr = codeObj._asyncCompYieldExpr;
355
+ }
356
+ // In 3.8+ inline async comprehensions the STORE_FAST lands
357
+ // inside a try/except block and disappears into the block
358
+ // structure before reaching SourceCode.list. Scan the inner
359
+ // code's instruction list: first STORE_* after GET_ANEXT is
360
+ // the loop variable.
361
+ if (!loopVar) {
362
+ const innerOps = new this.OpCodes(codeObj);
363
+ let sawAnext = false;
364
+ for (const op of innerOps.Instructions || []) {
365
+ if (!op) continue;
366
+ if (op.OpCodeID === this.OpCodes.GET_ANEXT) {
367
+ sawAnext = true;
368
+ continue;
369
+ }
370
+ if (sawAnext && (op.OpCodeID === this.OpCodes.STORE_FAST_A ||
371
+ op.OpCodeID === this.OpCodes.STORE_NAME_A ||
372
+ op.OpCodeID === this.OpCodes.STORE_DEREF_A)) {
373
+ const varName = op.Name?.toString?.() || '';
374
+ if (varName) {
375
+ loopVar = new AST.ASTName(varName);
376
+ isAsyncLoop = true;
377
+ break;
378
+ }
379
+ }
380
+ }
381
+ }
382
+ if (global.g_cliArgs?.debug) {
383
+ console.log(`[CALL-RECONSTRUCT] ${codeObj.Name?.toString?.()} yield=${yieldExpr?.constructor?.name} loopVar=${loopVar?.name} isAsyncLoop=${isAsyncLoop}`);
384
+ }
385
+ if (yieldExpr && loopVar) {
386
+ let kind = AST.ASTComprehension.GENERATOR;
387
+ const objName = codeObj.Name?.toString?.() || '';
388
+ if (objName.includes('listcomp')) kind = AST.ASTComprehension.LIST;
389
+ else if (objName.includes('setcomp')) kind = AST.ASTComprehension.SET;
390
+ else if (objName.includes('dictcomp')) kind = AST.ASTComprehension.DICT;
391
+ let comp;
392
+ if (kind === AST.ASTComprehension.DICT && codeObj._asyncCompYieldKey) {
393
+ comp = new AST.ASTComprehension(yieldExpr, codeObj._asyncCompYieldKey);
394
+ } else {
395
+ comp = new AST.ASTComprehension(yieldExpr);
396
+ }
397
+ comp.kind = kind;
398
+ const genBlockType = isAsyncLoop ? AST.ASTBlock.BlockType.AsyncFor : AST.ASTBlock.BlockType.For;
399
+ let gen = new AST.ASTIterBlock(genBlockType, 0, 0, new AST.ASTName('.0'));
400
+ gen.index = loopVar;
401
+ gen.comprehension = true;
402
+ comp.addGenerator(gen);
403
+ resultNode = comp;
404
+ }
405
+ }
406
+
167
407
  // Map placeholder iter (.0) to actual argument for comprehensions.
168
408
  if (resultNode?.generators) {
169
409
  if (global.g_cliArgs?.debug) {
@@ -177,18 +417,40 @@ function handleInstrumentedCallA() {
177
417
  if (global.g_cliArgs?.debug) {
178
418
  console.log(`[CALL] remapped iter .${paramIdx} -> ${gen.iter?.constructor?.name}`);
179
419
  }
420
+ } else if (gen.iter instanceof AST.ASTName && gen.iter.name === "[outmost-iterable]") {
421
+ // Py2.4 names the genexpr's first parameter `[outmost-iterable]`
422
+ const param = pparamList[0];
423
+ gen.iter = param instanceof AST.ASTIteratorValue ? param.value : param;
180
424
  }
181
425
  }
182
426
  }
183
427
 
184
428
  if (resultNode) {
429
+ // Async listcomp/setcomp/dictcomp: the outer function
430
+ // awaits the comprehension coroutine with GET_AWAITABLE +
431
+ // LOAD_CONST None + YIELD_FROM. Skip these — the await is
432
+ // implicit in the async comprehension syntax.
433
+ if (resultNode instanceof AST.ASTComprehension &&
434
+ resultNode.kind !== AST.ASTComprehension.GENERATOR) {
435
+ const nx1 = this.code.Next;
436
+ const nx2 = nx1?.Next;
437
+ const nx3 = nx2?.Next;
438
+ if (nx1 && nx2 && nx3 &&
439
+ (nx1.OpCodeID == this.OpCodes.GET_AWAITABLE ||
440
+ nx1.OpCodeID == this.OpCodes.GET_AWAITABLE_A) &&
441
+ nx2.OpCodeID == this.OpCodes.LOAD_CONST_A &&
442
+ nx3.OpCodeID == this.OpCodes.YIELD_FROM) {
443
+ this.code.GoNext(3);
444
+ }
445
+ }
185
446
  this.dataStack.push(resultNode);
186
447
  return;
187
448
  }
188
449
  }
189
450
  }
190
451
 
191
- if ([this.OpCodes.GET_ITER, this.OpCodes.GET_AITER].includes(this.code.Prev.OpCodeID)) {
452
+ if ([this.OpCodes.GET_ITER, this.OpCodes.GET_AITER].includes(this.code.Prev.OpCodeID) &&
453
+ func instanceof AST.ASTFunction && func.code?.object?.SourceCode?.list?.top) {
192
454
  let ast = func.code.object.SourceCode.list.top();
193
455
  if (!(ast instanceof AST.ASTKeyword)) {
194
456
  if (ast instanceof AST.ASTReturn) {
@@ -203,6 +465,12 @@ function handleInstrumentedCallA() {
203
465
  param = param.value;
204
466
  }
205
467
  gen.iter = param;
468
+ } else if (gen.iter instanceof AST.ASTName && gen.iter.name === "[outmost-iterable]") {
469
+ let param = pparamList[0];
470
+ if (param instanceof AST.ASTIteratorValue) {
471
+ param = param.value;
472
+ }
473
+ gen.iter = param;
206
474
  }
207
475
  }
208
476
  }
@@ -250,6 +518,66 @@ function handleCallFunctionVarA() {
250
518
  }
251
519
 
252
520
  function handleCallFunctionKwA() {
521
+ if (this.object.Reader.versionCompare(3, 6) >= 0) {
522
+ // Py 3.6+: CALL_FUNCTION_KW(argc) — TOS is a tuple of kwarg names,
523
+ // argc is total args, last len(names) are kwargs.
524
+ let kwnamesNode = this.dataStack.pop();
525
+ let kwNamesList = [];
526
+ const toKwName = (v) => {
527
+ const raw = v?.Value ?? v?.name ?? v;
528
+ const name = typeof raw === 'string' ? raw : String(raw);
529
+ return new AST.ASTName(name.replace(/^['"]|['"]$/g, ''));
530
+ };
531
+ if (kwnamesNode instanceof AST.ASTObject) {
532
+ const obj = kwnamesNode.object;
533
+ if (obj && (obj.ClassName === 'Py_Tuple' || obj.ClassName === 'Py_SmallTuple') && Array.isArray(obj.Value)) {
534
+ kwNamesList = obj.Value.map(toKwName);
535
+ }
536
+ } else if (kwnamesNode instanceof AST.ASTTuple) {
537
+ kwNamesList = (kwnamesNode.values || []).map(toKwName);
538
+ }
539
+
540
+ const totalArgs = this.code.Current.Argument;
541
+ const kwcount = kwNamesList.length;
542
+ const pcount = Math.max(0, totalArgs - kwcount);
543
+
544
+ let kwparamList = [];
545
+ for (let i = kwcount - 1; i >= 0; i--) {
546
+ let value = this.dataStack.pop();
547
+ kwparamList.unshift({ key: kwNamesList[i], value });
548
+ }
549
+
550
+ let pparamList = [];
551
+ for (let i = 0; i < pcount; i++) {
552
+ pparamList.unshift(this.dataStack.pop());
553
+ }
554
+
555
+ let func = this.dataStack.pop();
556
+
557
+ // 3.6+ class with kwargs uses CALL_FUNCTION_KW:
558
+ // LOAD_BUILD_CLASS / MAKE_FUNCTION / LOAD_CONST name / <bases>
559
+ // / LOAD_CONST (kwarg_names_tuple) / CALL_FUNCTION_KW argc
560
+ // pparamList is [body_func, class_name, ...bases]; kwparamList carries
561
+ // the class kwargs (metaclass, **__init_subclass__ kwargs, ...).
562
+ if (func instanceof AST.ASTLoadBuildClass && pparamList.length >= 2) {
563
+ const functionNode = pparamList[0];
564
+ const nameNode = pparamList[1];
565
+ const bases = pparamList.slice(2);
566
+ const classCall = new AST.ASTCall(functionNode, [], kwparamList);
567
+ classCall.line = this.code.Current.LineNo;
568
+ const classNode = new AST.ASTClass(classCall, new AST.ASTTuple(bases), nameNode);
569
+ classNode.line = this.code.Current.LineNo;
570
+ classNode.kwargs = kwparamList;
571
+ this.dataStack.push(classNode);
572
+ return;
573
+ }
574
+
575
+ let callNode = new AST.ASTCall(func, pparamList, kwparamList);
576
+ callNode.line = this.code.Current.LineNo;
577
+ this.dataStack.push(callNode);
578
+ return;
579
+ }
580
+
253
581
  let kw = this.dataStack.pop();
254
582
  let kwparams = (this.code.Current.Argument & 0xFF00) >> 8;
255
583
  let pparams = (this.code.Current.Argument & 0xFF);
@@ -339,34 +667,67 @@ function handleBinaryCall() {
339
667
  this.dataStack.push(callNode);
340
668
  }
341
669
 
670
+ function mapKeysAreIdentifiers(mapNode) {
671
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
672
+ for (const entry of mapNode.values || []) {
673
+ const keyObj = entry?.key;
674
+ const cls = keyObj?.object?.ClassName;
675
+ if (cls !== 'Py_String' && cls !== 'Py_Unicode') {
676
+ return false;
677
+ }
678
+ const s = keyObj.object.toString();
679
+ if (!IDENT.test(s)) {
680
+ return false;
681
+ }
682
+ }
683
+ return true;
684
+ }
685
+
342
686
  function handleCallFunctionExA() {
687
+ // CPython 3.6+: oparg & 0x01 means kwargs mapping is on TOS; the positional
688
+ // tuple and the callable are ALWAYS popped regardless of the flag.
343
689
  let flags = this.code.Current.Argument;
344
690
  let kwparams = [];
345
691
  let pparams = [];
692
+ let varArg = null;
693
+ let kwSpread = null;
694
+ let kwSingle = null;
346
695
  if (flags & 0x01) { // **kwargs
347
696
  let kw = this.dataStack.pop();
348
- if (kw instanceof AST.ASTMap) {
349
- kwparams = kw.values;
697
+ if (kw instanceof AST.ASTMapUnpack) {
698
+ // Py 3.6+ f(**a, **b) path: preserve individual ** sources for rendering.
699
+ kwSpread = kw;
700
+ } else if (kw instanceof AST.ASTMap) {
701
+ if (mapKeysAreIdentifiers(kw)) {
702
+ kwparams = kw.values;
703
+ } else {
704
+ // Non-identifier keys (e.g. '"', 'with space') must render as **{...}.
705
+ kwSingle = kw;
706
+ }
350
707
  } else if (kw?.object?.ClassName === "Py_Dict" && kw.object.Value) {
351
708
  kwparams = kw.object.Value.map(entry => ({key: new AST.ASTObject(entry.key), value: new AST.ASTObject(entry.value)}));
352
709
  } else {
353
- if (global.g_cliArgs?.debug) {
354
- console.error("Expected a map for CALL_FUNCTION_EX kwargs");
355
- }
710
+ // Single **dict argument where dict is a name/expression (not literal).
711
+ kwSingle = kw;
356
712
  }
357
713
  }
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
- }
714
+ let args = this.dataStack.pop();
715
+ if (args instanceof AST.ASTTuple || args instanceof AST.ASTList) {
716
+ pparams = args.values;
717
+ } else {
718
+ // *args as a non-literal expression (e.g. f(*tup_name, **kw)).
719
+ varArg = args;
367
720
  }
368
721
  let func = this.dataStack.pop();
369
722
  let callNode = new AST.ASTCall(func, pparams, kwparams);
723
+ if (varArg) {
724
+ callNode.var = varArg;
725
+ }
726
+ if (kwSpread) {
727
+ callNode.kw = kwSpread;
728
+ } else if (kwSingle) {
729
+ callNode.kw = kwSingle;
730
+ }
370
731
  callNode.line = this.code.Current.LineNo;
371
732
  this.dataStack.push(callNode);
372
733
  }
@@ -374,19 +735,15 @@ function handleCallFunctionExA() {
374
735
  function handleCallIntrinsic1() {
375
736
  // Python 3.12+ CALL_INTRINSIC_1 opcode
376
737
  // 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
738
 
380
- // Intrinsic takes 1 arg from stack, returns result
381
739
  let arg = this.dataStack.pop();
382
740
 
383
741
  if (global.g_cliArgs?.debug) {
384
742
  console.log(`[CALL_INTRINSIC_1] intrinsic=${this.code.Current.Argument}, arg=${arg?.constructor?.name}`);
385
743
  }
386
744
 
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) {
745
+ // PEP 695 type statement wraps tuple(name, qualname, type-fn)
746
+ if (this.code.Current.Argument === INTRINSIC_1.TYPEALIAS && arg instanceof AST.ASTTuple) {
390
747
  const values = arg.values || [];
391
748
  const aliasName = values[0]?.object?.Value || values[0]?.name || values[0]?.codeFragment?.();
392
749
  const typeFunc = values[2];
@@ -432,9 +789,9 @@ function handleCallIntrinsic2() {
432
789
  const argA = this.dataStack.pop();
433
790
  const args = [argA, argB];
434
791
 
435
- // Intrinsic 1: PREP_RERAISE_STAR equivalent in 3.12+.
792
+ // PREP_RERAISE_STAR equivalent in 3.12+.
436
793
  // Do not emit; mark to skip the following conditional jump.
437
- if (this.code.Current.Argument === 1) {
794
+ if (this.code.Current.Argument === INTRINSIC_2.PREP_RERAISE_STAR) {
438
795
  this.ignoreNextConditional = true;
439
796
  this.cleanupStackDepth = this.dataStack.length + 1; // after we push a placeholder
440
797
  this.dataStack.push(new AST.ASTNone());
@@ -442,23 +799,81 @@ function handleCallIntrinsic2() {
442
799
  return;
443
800
  }
444
801
 
445
- // Intrinsic 4: build generic function from type parameters + function
446
- if (this.code.Current.Argument === 4) {
802
+ // SET_FUNCTION_TYPE_PARAMS build generic function from
803
+ // type parameters tuple + function
804
+ if (this.code.Current.Argument === INTRINSIC_2.SET_FUNCTION_TYPE_PARAMS) {
447
805
  const funcNode = args.find(a => a instanceof AST.ASTFunction);
448
806
  const typeArg = args.find(a => a instanceof AST.ASTTuple || a instanceof AST.ASTList);
449
807
  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';
808
+ const params = (typeArg.values || []).map(v => {
809
+ // COPY/STORE_FAST inside the wrapper wraps each typevar in a
810
+ // walrus ASTNamedExpr(name := typevar). Unwrap to reach the
811
+ // underlying typevar (which carries pepDefault for PEP 696).
812
+ let source = v;
813
+ if (v instanceof AST.ASTNamedExpr) {
814
+ source = v.value;
815
+ }
816
+ let name;
817
+ if (v instanceof AST.ASTNamedExpr && v.target?.name) {
818
+ name = v.target.name;
819
+ } else if (source instanceof AST.ASTName) {
820
+ name = source.name;
821
+ } else if (source?.object?.Value) {
822
+ name = source.object.Value;
823
+ } else {
824
+ const frag = source?.codeFragment?.();
825
+ name = frag?.toString?.() || 'T';
826
+ }
827
+ if (source?.pepDefault != null) {
828
+ return { name, default: source.pepDefault };
829
+ }
830
+ return name;
455
831
  });
456
- funcNode.typeParams = names;
832
+ funcNode.typeParams = params;
457
833
  this.dataStack.push(funcNode);
458
834
  return;
459
835
  }
460
836
  }
461
837
 
838
+ // SET_TYPEPARAM_DEFAULT (PEP 696, 3.13+)
839
+ // Stack: [..., typevar, default_fn] → [..., typevar_with_default]
840
+ // The default wrapper is a zero-arg function whose return value is the default.
841
+ if (this.code.Current.Argument === INTRINSIC_2.SET_TYPEPARAM_DEFAULT) {
842
+ const defaultFn = argB;
843
+ const typevar = argA;
844
+ let defaultValue = null;
845
+ const extractReturn = (body) => {
846
+ const list = body?.list || [];
847
+ for (let i = list.length - 1; i >= 0; i--) {
848
+ if (list[i] instanceof AST.ASTReturn && list[i].value) {
849
+ return list[i].value;
850
+ }
851
+ }
852
+ return null;
853
+ };
854
+ if (defaultFn instanceof AST.ASTFunction) {
855
+ defaultValue = extractReturn(defaultFn.code?.object?.SourceCode);
856
+ } else if (defaultFn instanceof AST.ASTObject &&
857
+ (defaultFn.object?.ClassName === 'Py_CodeObject' ||
858
+ defaultFn.object?.ClassName === 'Py_CodeObject2')) {
859
+ try {
860
+ const PycDecompiler = require('../PycDecompiler');
861
+ const dd = new PycDecompiler(defaultFn.object);
862
+ const body = dd.decompile();
863
+ defaultFn.object.SourceCode = body;
864
+ if (dd.errors.length) this.errors.push(...dd.errors);
865
+ defaultValue = extractReturn(body);
866
+ } catch (_) { /* bare typevar */ }
867
+ }
868
+ if (typevar != null && defaultValue != null) {
869
+ typevar.pepDefault = defaultValue;
870
+ }
871
+ if (typevar !== undefined) {
872
+ this.dataStack.push(typevar);
873
+ }
874
+ return;
875
+ }
876
+
462
877
  // Default: preserve the left argument to avoid stack underflow
463
878
  if (argA !== undefined) {
464
879
  this.dataStack.push(argA);
@@ -478,6 +893,7 @@ function handleCallIntrinsic2A() {
478
893
  module.exports = {
479
894
  handleKwNamesA,
480
895
  handleCallA,
896
+ handleCallKwA,
481
897
  handleInstrumentedCallKwA,
482
898
  handleCallFunctionA,
483
899
  handleInstrumentedCallA,