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.
package/depyo.js CHANGED
@@ -22,6 +22,7 @@ global.g_cliArgs = {
22
22
  sendToStdout: false,
23
23
  marshal: false,
24
24
  marshalScan: false,
25
+ strict: false,
25
26
  pyVersion: null,
26
27
  silent: false,
27
28
  fileExt: 'py',
@@ -29,6 +30,8 @@ global.g_cliArgs = {
29
30
  filenames: []
30
31
  };
31
32
 
33
+ let g_dirtyFiles = []; // Files where decompiler caught at least one opcode exception.
34
+
32
35
  let g_totalInThroughput = 0;
33
36
  let g_totalOutThroughput = 0;
34
37
  let g_totalExecTime = 0;
@@ -51,6 +54,7 @@ Options:
51
54
  --out Print decompiled source to stdout instead of files
52
55
  --marshal Treat input as raw marshalled data (no .pyc header)
53
56
  --marshal-scan Fast scan of marshal blobs (no decompile, prints version)
57
+ --strict Re-throw on first opcode handler exception (default: log + continue + non-zero exit)
54
58
  --py-version <x.y> Python bytecode version hint (auto-scan if omitted)
55
59
  --basedir <path> Output base directory (default: alongside input)
56
60
  --file-ext <ext> Extension for generated source (default: py)
@@ -83,6 +87,8 @@ function parseCLIParams() {
83
87
  } else if (cliParam.toLowerCase() == "--marshal-scan" || cliParam.toLowerCase() == "--marshal-smoke") {
84
88
  g_cliArgs.marshalScan = true;
85
89
  g_cliArgs.marshal = true;
90
+ } else if (cliParam.toLowerCase() == "--strict") {
91
+ g_cliArgs.strict = true;
86
92
  } else if (cliParam.toLowerCase() == "--py-version") {
87
93
  g_cliArgs.pyVersion = process.argv[++idx];
88
94
  } else if (cliParam.toLowerCase() == "--basedir") {
@@ -341,9 +347,33 @@ function decompilePycObject(data) {
341
347
  let genStartTS = process.hrtime.bigint();
342
348
  let decompiler = new PycDecompiler(obj);
343
349
  let ast = decompiler.decompile();
344
- let pycResult = ast.codeFragment();
345
- pySrc = pycResult.toString();
350
+ let renderError = null;
351
+ try {
352
+ let pycResult = ast.codeFragment();
353
+ pySrc = pycResult.toString();
354
+ } catch (ex) {
355
+ if (g_cliArgs.strict) throw ex;
356
+ renderError = ex;
357
+ decompiler.errors.push({
358
+ opcode: 'RENDER',
359
+ codeObject: obj?.Name?.toString?.() || '<root>',
360
+ message: ex.message,
361
+ stack: ex.stack
362
+ });
363
+ decompiler.cleanBuild = false;
364
+ pySrc = `# DECOMPILER ERROR: codeFragment() threw: ${ex.message}\n`;
365
+ if (!g_cliArgs.silent) {
366
+ console.error(`RENDER EXCEPTION in '${obj?.Name}': ${ex.message}`);
367
+ if (g_cliArgs.debug) console.error(ex.stack);
368
+ }
369
+ }
346
370
  genSecs = Number(process.hrtime.bigint() - genStartTS) / 1000000000;
371
+ if (!decompiler.cleanBuild) {
372
+ g_dirtyFiles.push({
373
+ file: typeof data === 'string' ? data : (obj?.FileName || '<buffer>'),
374
+ errors: decompiler.errors.length
375
+ });
376
+ }
347
377
  }
348
378
  if (!pySrc.endsWith("\n")) {
349
379
  pySrc += "\n";
@@ -434,3 +464,14 @@ if (!g_cliArgs.sendToStdout) {
434
464
  const outRate = (g_totalOutThroughput / g_totalExecTime).toFixed(2);
435
465
  console.log(`Processed ${g_totalFiles} files in ${g_totalExecTime.toFixed(3)}s. In: ${g_totalInThroughput} bytes (${inRate} B/s). Out: ${g_totalOutThroughput} bytes (${outRate} B/s).`);
436
466
  }
467
+
468
+ if (g_dirtyFiles.length > 0) {
469
+ console.error(`\nDirty decompile: ${g_dirtyFiles.length} file(s) had handler exceptions (output may be partial):`);
470
+ for (const d of g_dirtyFiles.slice(0, 20)) {
471
+ console.error(` - ${d.file} (${d.errors} opcode error${d.errors === 1 ? '' : 's'})`);
472
+ }
473
+ if (g_dirtyFiles.length > 20) {
474
+ console.error(` ... and ${g_dirtyFiles.length - 20} more`);
475
+ }
476
+ process.exit(1);
477
+ }
package/lib/OpCodes.js CHANGED
@@ -329,6 +329,9 @@ class OpCodes
329
329
  static LOAD_ZERO_SUPER_ATTR_A = 319; // Python 3.13+ zero-cost super attr
330
330
  static LOAD_ZERO_SUPER_METHOD_A = 320; // Python 3.13+ zero-cost super method
331
331
 
332
+ // Python 3.15 new opcodes
333
+ static TRACE_RECORD_A = 321; // Python 3.15 -> trace recording (ignore)
334
+
332
335
 
333
336
  // enum cmp_op
334
337
  // {
@@ -391,7 +394,7 @@ class OpCodes
391
394
 
392
395
  if (reader.versionCompare(3, 6) >= 0) {
393
396
  while (opCodeID == OpCodes.EXTENDED_ARG_A) {
394
- argument = argument | code[++opOffset] << 8;
397
+ argument = (argument << 8) | code[++opOffset];
395
398
  opCodeID = this.GetOpCodeID(code, ++opOffset);
396
399
 
397
400
  // Break if we hit end of bytecode
@@ -401,9 +404,15 @@ class OpCodes
401
404
  }
402
405
  argument <<= 8;
403
406
  } else {
407
+ // Pre-3.6: EXTENDED_ARG carries a 16-bit operand that becomes
408
+ // the upper 16 bits of the next instruction's argument. After
409
+ // reading both operand bytes we must advance opOffset one more
410
+ // step so the caller lands on the real opcode (not the trailing
411
+ // operand byte of EXTENDED_ARG).
404
412
  if (opCodeID == OpCodes.EXTENDED_ARG_A) {
405
413
  argument = code[++opOffset] | code[++opOffset] << 8;
406
414
  argument <<= 16;
415
+ opOffset++;
407
416
  }
408
417
  }
409
418
 
@@ -522,10 +531,18 @@ class OpCodes
522
531
  opCode.Name = this.CodeObject.Names.Value[opCode.Argument].toString();
523
532
  }
524
533
  } else if (opCode.HasFree) {
525
- if (opCode.Argument < this.CodeObject.CellVars.Value.length) {
526
- opCode.FreeName = this.CodeObject.CellVars.Value[opCode.Argument].toString();
527
- } else if ((opCode.Argument - this.CodeObject.CellVars.Value.length) < this.CodeObject.FreeVars.Value.length) {
528
- opCode.FreeName = this.CodeObject.FreeVars.Value[opCode.Argument - this.CodeObject.CellVars.Value.length].toString();
534
+ // 3.11+ stores cells/frees inside localsplus, so the opcode argument
535
+ // is an index into [locals | cells | frees]. Strip the locals prefix
536
+ // before looking up into the split CellVars/FreeVars tuples.
537
+ const isNewLayout = this.CodeObject.Reader?.versionCompare?.(3, 11) >= 0;
538
+ const localsLen = isNewLayout ? (this.CodeObject.VarNames?.Value?.length ?? 0) : 0;
539
+ const freeIdx = opCode.Argument - localsLen;
540
+ const cellLen = this.CodeObject.CellVars?.Value?.length ?? 0;
541
+ const freeLen = this.CodeObject.FreeVars?.Value?.length ?? 0;
542
+ if (freeIdx >= 0 && freeIdx < cellLen) {
543
+ opCode.FreeName = this.CodeObject.CellVars.Value[freeIdx].toString();
544
+ } else if (freeIdx >= cellLen && (freeIdx - cellLen) < freeLen) {
545
+ opCode.FreeName = this.CodeObject.FreeVars.Value[freeIdx - cellLen].toString();
529
546
  } else {
530
547
  opCode.FreeName = `##FREEVAR_${opCode.Argument}##`;
531
548
  }