Skip to content

#27 Add prescan phase for short-circuit boolean expression decompilation - #31

Merged
sahebbiswas merged 7 commits into
mainfrom
27-compound-if-statements-are-incorrectly-placed-in-multiple-lines
Mar 27, 2026
Merged

#27 Add prescan phase for short-circuit boolean expression decompilation#31
sahebbiswas merged 7 commits into
mainfrom
27-compound-if-statements-are-incorrectly-placed-in-multiple-lines

Conversation

@sahebbiswas

@sahebbiswas sahebbiswas commented Mar 27, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Decompiler now recognizes and reconstructs compound boolean chains (and/or, including is None/is not None, indexing/function-call clauses), emitting clearer combined if/while headers and improved merged-header output.
    • Ternary/conditional expressions handle subscription-style formatting more accurately.
  • Bug Fixes

    • Removed extra trailing newline at end-of-file in output.
  • Tests

    • Added end-to-end tests for compound conditions, short-circuiting, precedence, indexing/calls, and a nested-if regression.

@sahebbiswas sahebbiswas linked an issue Mar 27, 2026 that may be closed by this pull request
@sahebbiswas sahebbiswas self-assigned this Mar 27, 2026
@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

A prescan pass detects contiguous POP_JUMP_IF_* short‑circuit chains, evaluates and assembles combined boolean-condition strings, records suppressed clause offsets, and the instruction handler uses these maps to skip intermediate emits and produce single combined if/while headers during decompilation.

Changes

Cohort / File(s) Summary
Decompiler core
pycrefine.py
Added _prescan_compound_conds() plus helpers _is_compound_cjump() and _eval_cond_expr(); decompile() now runs the compound prescan. Introduced _compound_cond_map and _compound_suppress. _prescan_ternaries() docstring reworded. _eval_ternary_branch() treats BINARY_OP arg 26 as subscription. _handle_instruction() skips/suppresses compound offsets (pops stack to keep balance), routes POP_JUMP_IF_* through a shared conditional path using _is_compound_cjump(), and prefers precomputed compound conditions when emitting if/while headers. Removed separate POP_JUMP_IF_NONE handling. Minor main() EOF newline removal.
Tests
tests/test_pycrefine.py
Added TestCompoundConditions(unittest.TestCase) with end-to-end tests for and/or chains (including is None/is not None), mixed precedence/parenthesisation, indexing/call clauses, short-circuit behavior, and a nested-if merge regression. Normalized an existing docstring/line endings and moved the module entry-point to the bottom.

Sequence Diagram(s)

sequenceDiagram
    participant Decompiler as Decompiler (decompile)
    participant Prescan as Prescan (_prescan_compound_conds)
    participant Handler as InstructionHandler (_handle_instruction)
    participant Emitter as Emitter (emit)

    Decompiler->>Prescan: scan bytecode for POP_JUMP_IF_* chains
    Prescan-->>Decompiler: return _compound_cond_map and _compound_suppress
    Decompiler->>Handler: iterate instructions
    Handler->>Handler: if offset in _compound_suppress -> pop stack, skip emit
    Handler->>Emitter: if offset in _compound_cond_map -> emit combined condition header
    Handler->>Emitter: else -> emit normal instruction handling
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

In quiet scan the clauses join as one,
Each fragment hushed until the header’s done.
The middle yields, the single truth is shown,
Soft maps align, and decompiled code walks home.
Peace in small passes — bytecode sings, not roams.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed Title check skipped as CodeRabbit has written the PR title.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 27-compound-if-statements-are-incorrectly-placed-in-multiple-lines

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot changed the title #27 @coderabbitai #27 Add prescan phase for short-circuit boolean expression decompilation Mar 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pycrefine.py`:
- Around line 957-1007: The _is_compound_cjump check must be extended to
recognize Python 3.11+ opnames (e.g. POP_JUMP_FORWARD_IF_TRUE /
POP_JUMP_FORWARD_IF_FALSE / POP_JUMP_FORWARD_IF_NONE /
POP_JUMP_FORWARD_IF_NOT_NONE) in addition to the existing POP_JUMP_IF_* variants
so the prescan triggers on 3.11+. Update the method _is_compound_cjump to
include those OP name patterns and any helpers that rely on it
(_get_jump_target, the prescan loop that builds group) so group detection runs
on modern bytecode; then adjust the reconstruction in the cond-combining code
(the block that calls _eval_cond_expr and builds parts/combined) to preserve
subgroup boundaries for (a or b) and c vs a and (b or c) by keeping connectors
local to each subgroup rather than flattening across differing connectors, and
add regression tests exercising both forms across Python 3.10–3.13 to ensure
semantic equivalence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1cc747ee-fe37-456b-a19e-e4f8b9eb8df9

📥 Commits

Reviewing files that changed from the base of the PR and between 8a18dd4 and 793e098.

📒 Files selected for processing (2)
  • pycrefine.py
  • tests/test_pycrefine.py

Comment thread pycrefine.py Outdated
…ions by correctly applying operator precedence and parenthesization.
@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Note

Docstrings generation - SUCCESS
Generated docstrings and committed to branch 27-compound-if-statements-are-incorrectly-placed-in-multiple-lines (commit: 7305ae6a3310ebb781459b00d0dfcad3f9699fa3)

…-in-multiple-lines`

Docstrings generation was requested by @sahebbiswas.

The following files were modified:

* `pycrefine.py`
* `tests/test_pycrefine.py`

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
pycrefine.py (1)

1023-1055: ⚠️ Potential issue | 🔴 Critical

Dear friend, this combiner still changes mixed and/or semantics.

combined_contains_or is set once and never updated, so a later and wraps the whole accumulated expression instead of the local subgroup. That makes shapes like a or b and c decompile as (a or b) and c, which is a different program. Please derive subgroup boundaries from the jump targets rather than from one global or flag, and add a regression for both a or b and c and (a or b) and c.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pycrefine.py` around lines 1023 - 1055, The current linear combiner in the
loop (using variables combined, combined_contains_or, prev_was_or and conn over
parts) uses a single combined_contains_or flag that is never updated, causing
incorrect parenthesization for mixed and/or sequences; modify the builder inside
the for k in range(...) loop to compute subgroup boundaries from the parts' jump
targets (i.e., inspect parts[k] and parts[k+1] metadata or explicit jump target
info) instead of relying on the global combined_contains_or flag, update the
local flag/state as you append each next_expr (so parentheses are applied only
to the immediate subgroup), and remove/replace the stale combined_contains_or
usage; add regression tests that validate both "a or b and c" and "(a or b) and
c" decompilation results to ensure correct precedence handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pycrefine.py`:
- Around line 2176-2187: When a precomputed normalized condition is pulled from
self._compound_cond_map (via compound_cond = compound_cond_map.get(instr.offset)
and assigned to cond), mark that this condition is precomputed (e.g., set a flag
like compound_precomputed = True) and ensure the subsequent is_true rewrite
branch (the logic that inverts or appends "is True"/"is not None") checks this
flag and skips modifying cond if compound_precomputed is True; update the code
paths around compound_cond, instr.offset, and the is_true handling to respect
this flag so precomputed conditions from _prescan_compound_conds() are not
rewritten a second time.

---

Duplicate comments:
In `@pycrefine.py`:
- Around line 1023-1055: The current linear combiner in the loop (using
variables combined, combined_contains_or, prev_was_or and conn over parts) uses
a single combined_contains_or flag that is never updated, causing incorrect
parenthesization for mixed and/or sequences; modify the builder inside the for k
in range(...) loop to compute subgroup boundaries from the parts' jump targets
(i.e., inspect parts[k] and parts[k+1] metadata or explicit jump target info)
instead of relying on the global combined_contains_or flag, update the local
flag/state as you append each next_expr (so parentheses are applied only to the
immediate subgroup), and remove/replace the stale combined_contains_or usage;
add regression tests that validate both "a or b and c" and "(a or b) and c"
decompilation results to ensure correct precedence handling.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1a5ca38d-41fa-48e6-a0bc-db3d7a434eae

📥 Commits

Reviewing files that changed from the base of the PR and between 793e098 and a2ab2b0.

📒 Files selected for processing (2)
  • pycrefine.py
  • tests/test_pycrefine.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_pycrefine.py

Comment thread pycrefine.py
@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • pycrefine.py

Commit: 57f81cadfbec7a7afdfa97f8577f38f2ec59573c

The changes have been pushed to the 27-compound-if-statements-are-incorrectly-placed-in-multiple-lines branch.

Time taken: 4m 53s

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
pycrefine.py (2)

1006-1038: ⚠️ Potential issue | 🔴 Critical

Kindly preserve subgroup boundaries instead of flattening the chain.

This left-to-right builder still changes semantics for mixed short-circuit groups. a and (b or c) becomes a and b or c, while a or b and c can be forced into (a or b) and c. One precedence flag cannot distinguish those bytecode shapes; this needs grouping by jump target/sub-expression, with regressions for a and (b or c) and (a or b) and c.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pycrefine.py` around lines 1006 - 1038, The builder flattens mixed
short-circuit groups because it only tracks a single precedence flag; update the
combine loop to preserve subgroup boundaries (use the subgroup/jump-target info
stored in parts rather than a single combined_contains_or flag). Specifically,
change the logic around combined, parts, curr_is_or, next_is_or and prev_was_or
so you treat any part that represents a grouped sub-expression as atomic (wrap
it in parens when it came from a subgroup/jump-target) and when joining use the
actual next_is_or/curr_is_or pair to decide wrapping instead of a single
top-level flag; ensure you never transform a true subgroup (e.g., a and (b or c)
or (a or b) and c) into a flattened chain by consulting each part's subgroup
marker before concatenation.

2190-2203: ⚠️ Potential issue | 🔴 Critical

Kindly skip the second inversion when cond came from _compound_cond_map.

Once cond is replaced with a precomputed compound expression, the later is_true branch still negates it again. if a or not b: then emits as if not a or not b:, which is a different predicate.

Suggested fix
                 compound_cond_map = getattr(self, "_compound_cond_map", {})
                 compound_cond = compound_cond_map.get(instr.offset)
-                if compound_cond is not None:
+                compound_precomputed = compound_cond is not None
+                if compound_precomputed:
                     cond = compound_cond
                 else:
                     if "IF_NONE" in opname and "NOT" not in opname:
                         # Fires on None; body runs on NOT None
                         cond = f"{cond} is not None"
                     elif "IF_NOT_NONE" in opname:
                         # Fires on NOT None; body runs on None
                         cond = f"{cond} is None"
 
-                is_true = "IF_TRUE" in opname
+                is_true = "IF_TRUE" in opname and not compound_precomputed
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pycrefine.py` around lines 2190 - 2203, When a precomputed condition from
_compound_cond_map (compound_cond) is used to set cond, skip any further
inversion logic: do not run the IF_NONE/IF_NOT_NONE branches nor apply the
is_true negation; instead leave cond as the compound expression. Concretely, in
the block around compound_cond_map/_compound_cond_map and opname, ensure that if
compound_cond is not None you set cond and bypass the code that mutates cond
based on "IF_NONE"/"IF_NOT_NONE" and the is_true ("IF_TRUE" in opname) handling
that currently flips the predicate; only run those transformations when
compound_cond is None. This prevents double-negation cases like when a compound
cond has already encoded the correct polarity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pycrefine.py`:
- Around line 971-991: The fallback that sets is_or_jump = is_success_type
(based solely on "IF_TRUE" in op) misclassifies mixed None-chains; instead,
determine polarity for intermediate jumps by examining the grouped jump
structure/subgroup membership rather than raw truthiness: when t is an
intermediate target, consult the subgroup start/edges that t belongs to (the
jump chain that includes body_target/end_target) and derive whether a None-edge
represents success or failure for that subgroup (use the existing grouping logic
that identifies subgroup boundaries rather than the is_success_type flag);
update the branch that handles "IF_NONE"/"IF_NOT_NONE" to use that derived
subgroup polarity to set cond_str, and add a regression test covering patterns
like (x is None or y) and z to prevent future regressions.

---

Duplicate comments:
In `@pycrefine.py`:
- Around line 1006-1038: The builder flattens mixed short-circuit groups because
it only tracks a single precedence flag; update the combine loop to preserve
subgroup boundaries (use the subgroup/jump-target info stored in parts rather
than a single combined_contains_or flag). Specifically, change the logic around
combined, parts, curr_is_or, next_is_or and prev_was_or so you treat any part
that represents a grouped sub-expression as atomic (wrap it in parens when it
came from a subgroup/jump-target) and when joining use the actual
next_is_or/curr_is_or pair to decide wrapping instead of a single top-level
flag; ensure you never transform a true subgroup (e.g., a and (b or c) or (a or
b) and c) into a flattened chain by consulting each part's subgroup marker
before concatenation.
- Around line 2190-2203: When a precomputed condition from _compound_cond_map
(compound_cond) is used to set cond, skip any further inversion logic: do not
run the IF_NONE/IF_NOT_NONE branches nor apply the is_true negation; instead
leave cond as the compound expression. Concretely, in the block around
compound_cond_map/_compound_cond_map and opname, ensure that if compound_cond is
not None you set cond and bypass the code that mutates cond based on
"IF_NONE"/"IF_NOT_NONE" and the is_true ("IF_TRUE" in opname) handling that
currently flips the predicate; only run those transformations when compound_cond
is None. This prevents double-negation cases like when a compound cond has
already encoded the correct polarity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ed546f90-b072-4815-8880-029554ca6907

📥 Commits

Reviewing files that changed from the base of the PR and between a2ab2b0 and 7305ae6.

📒 Files selected for processing (2)
  • pycrefine.py
  • tests/test_pycrefine.py

Comment thread pycrefine.py
Comment on lines +971 to +991
is_success_type = ("IF_TRUE" in op)

# Logic: if jump targets body (OR), it's definitely an OR jump.
# If it targets END, it's definitely an AND jump.
# If it targets a later part of the chain:
# - Success-type jump to later = Progress to next OR sub-group.
# - Failure-type jump to later = Progress to next part of AND-chain.
if t == body_target:
is_or_jump = True
elif t == end_target:
is_or_jump = False
else:
# Intermediate target.
is_or_jump = is_success_type

if "IF_NONE" in op and "NOT" not in op:
# Fires on None; if OR jump, success is None. If AND, failure is None -> success is not None.
cond_str = f"{raw_expr} is None" if is_or_jump else f"{raw_expr} is not None"
elif "IF_NOT_NONE" in op:
# Fires on NOT None; if OR jump, success is NOT None. If AND, failure is NOT None -> success is None.
cond_str = f"{raw_expr} is not None" if is_or_jump else f"{raw_expr} is None"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Kindly do not infer every intermediate jump from IF_TRUE alone.

The fallback here still misclassifies mixed None-chains that jump to a later clause start. A shape like (x is None or y) and z can be reconstructed with the first clause inverted, because the intermediate IF_NONE / IF_NOT_NONE edge carries subgroup meaning, not just raw truthiness. Please add a regression in this family and derive that polarity from the grouped jump structure instead of "IF_TRUE" alone.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pycrefine.py` around lines 971 - 991, The fallback that sets is_or_jump =
is_success_type (based solely on "IF_TRUE" in op) misclassifies mixed
None-chains; instead, determine polarity for intermediate jumps by examining the
grouped jump structure/subgroup membership rather than raw truthiness: when t is
an intermediate target, consult the subgroup start/edges that t belongs to (the
jump chain that includes body_target/end_target) and derive whether a None-edge
represents success or failure for that subgroup (use the existing grouping logic
that identifies subgroup boundaries rather than the is_success_type flag);
update the branch that handles "IF_NONE"/"IF_NOT_NONE" to use that derived
subgroup polarity to set cond_str, and add a regression test covering patterns
like (x is None or y) and z to prevent future regressions.

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pycrefine.py (1)

2266-2280: ⚠️ Potential issue | 🟡 Minor

Dear friend, this code will never see the light of execution.

Lines 2146-2151 already handle POP_JUMP_IF_NONE and POP_JUMP_IF_NOT_NONE within the main conditional block. This subsequent elif block at lines 2266-2280 checking for the same opcodes is unreachable dead code, as the earlier condition will always match first.

In the pursuit of truth and simplicity, I humbly suggest removing this unreachable code.

🗑️ Suggested removal
-        # FIX-01: POP_JUMP_IF_NONE / POP_JUMP_IF_NOT_NONE — was swapped
-        elif "POP_JUMP_IF_NONE" in opname or "POP_JUMP_IF_NOT_NONE" in opname:
-            if self.stack:
-                cond = self.stack.pop()
-                is_not_none = "NOT_NONE" in opname
-                # POP_JUMP_IF_NOT_NONE: jumps when not-None → body runs when None
-                # POP_JUMP_IF_NONE:     jumps when None     → body runs when not-None
-                if is_not_none:
-                    self._append_reconstructed(f"if {cond} is None:")
-                else:
-                    self._append_reconstructed(f"if {cond} is not None:")
-
-                self.indent_level += 1
-                jump_target = self._get_jump_target(instr)
-                self.blocks.append((jump_target, "if"))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pycrefine.py` around lines 2266 - 2280, The elif block that tests for
"POP_JUMP_IF_NONE" / "POP_JUMP_IF_NOT_NONE" (which pops self.stack, sets
is_not_none, calls self._append_reconstructed, increments self.indent_level,
computes jump_target via self._get_jump_target, and appends to self.blocks) is
unreachable because those opcodes are already handled earlier in the main
conditional; remove this entire duplicate elif branch to eliminate dead code and
avoid double-handling of opname related to POP_JUMP_IF_NONE /
POP_JUMP_IF_NOT_NONE.
🧹 Nitpick comments (1)
pycrefine.py (1)

961-961: Unused loop variables, my friend.

The loop control variables k and jump_idx are unpacked but never used within the loop body. In the spirit of simplicity and clarity, consider renaming them with underscore prefixes to indicate they are intentionally unused.

🔧 Suggested fix
-            for k, (jump_idx, jinstr, expr_instrs, _) in enumerate(group):
+            for _k, (_jump_idx, jinstr, expr_instrs, _) in enumerate(group):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pycrefine.py` at line 961, The loop unpacks unused variables k and jump_idx;
rename them to indicate they are intentionally unused (e.g., _k and _jump_idx or
_ and _jump_idx) in the for statement "for k, (jump_idx, jinstr, expr_instrs, _)
in enumerate(group):" so the intent is clear and lint warnings are
suppressed—update any references (there should be none) and run tests/linter to
confirm no other uses exist.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pycrefine.py`:
- Around line 1006-1037: The loop's parenthesis tracking is broken because
next_is_or is never used, prev_was_or is assigned but not consulted, and
combined_contains_or is never updated after initialization; fix the for-loop
that builds combined (referencing variables combined, combined_contains_or,
prev_was_or, curr_is_or, next_is_or, conn, next_expr) by updating
combined_contains_or whenever an OR is encountered (e.g., combined_contains_or =
combined_contains_or or curr_is_or or next_is_or), actually using prev_was_or to
record the previous connector state and setting it each iteration (prev_was_or =
conn == "or"), and ensure the wrapping check before appending uses the
up-to-date combined_contains_or (if conn == "and" and combined_contains_or then
wrap combined); remove any redundant assignments if made unnecessary by this
logic.

---

Outside diff comments:
In `@pycrefine.py`:
- Around line 2266-2280: The elif block that tests for "POP_JUMP_IF_NONE" /
"POP_JUMP_IF_NOT_NONE" (which pops self.stack, sets is_not_none, calls
self._append_reconstructed, increments self.indent_level, computes jump_target
via self._get_jump_target, and appends to self.blocks) is unreachable because
those opcodes are already handled earlier in the main conditional; remove this
entire duplicate elif branch to eliminate dead code and avoid double-handling of
opname related to POP_JUMP_IF_NONE / POP_JUMP_IF_NOT_NONE.

---

Nitpick comments:
In `@pycrefine.py`:
- Line 961: The loop unpacks unused variables k and jump_idx; rename them to
indicate they are intentionally unused (e.g., _k and _jump_idx or _ and
_jump_idx) in the for statement "for k, (jump_idx, jinstr, expr_instrs, _) in
enumerate(group):" so the intent is clear and lint warnings are
suppressed—update any references (there should be none) and run tests/linter to
confirm no other uses exist.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5efbeb57-bec8-442d-80a2-57e2a213a1ca

📥 Commits

Reviewing files that changed from the base of the PR and between 7305ae6 and 57f81ca.

📒 Files selected for processing (1)
  • pycrefine.py

Comment thread pycrefine.py Outdated
…ly handle operator precedence and parenthesization, and add a comprehensive test suite including specific precedence tests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
pycrefine.py (1)

982-984: ⚠️ Potential issue | 🔴 Critical

Intermediate-target polarity is still inferred from IF_TRUE alone.

Dear friend, this keeps the same semantic risk for mixed None-chains (for example, shapes like (x is None or y) and z) when the jump target is intermediate, not final. Please derive is_or_jump from subgroup structure/target boundaries instead of is_success_type fallback at Line 983.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pycrefine.py` around lines 982 - 984, The assignment of is_or_jump currently
uses is_success_type as a fallback which misclassifies intermediate jump
targets; change the logic so is_or_jump is computed from the subgroup/target
boundary information instead: inspect the current subgroup structure and the
jump target's boundary (whether the target is an intermediate node within the
same subgroup vs a final exit) and set is_or_jump true only when the
subgroup/target relation indicates an OR-style short-circuit jump, otherwise
false; replace the fallback use of is_success_type at the is_or_jump assignment
with this boundary/subgroup-derived check so mixed None-chains are handled
correctly.
🧹 Nitpick comments (2)
pycrefine.py (1)

2289-2304: Legacy POP_JUMP_IF_NONE branch is now unreachable.

Because Lines 2169-2174 already capture POP_JUMP_IF_NONE / POP_JUMP_IF_NOT_NONE, this later branch no longer executes. Removing it will reduce confusion and maintenance risk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pycrefine.py` around lines 2289 - 2304, Remove the unreachable redundant
branch that handles POP_JUMP_IF_NONE / POP_JUMP_IF_NOT_NONE: delete the entire
elif block that checks '"POP_JUMP_IF_NONE" in opname or "POP_JUMP_IF_NOT_NONE"
in opname' (the block that pops self.stack, sets is_not_none, calls
self._append_reconstructed, increments self.indent_level, computes jump_target
via self._get_jump_target(instr), and appends to self.blocks) because those
opnames are already handled earlier; ensure no other code paths rely on this
block before removing it.
tests/test_pycrefine.py (1)

2800-2864: Prefer counting real if headers instead of out.count("if ").

Using count("if ") may overcount occurrences inside expressions/strings. A line-based header counter will make these tests steadier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_pycrefine.py` around lines 2800 - 2864, Replace fragile uses of
out.count("if ") in the test methods (e.g. test_compound_or,
test_compound_mixed_and_or, test_compound_none_and, test_compound_none_or,
test_compound_complex_mixed, test_compound_short_circuit_with_call,
test_compound_nested_if_merge_regression) with a line-based header counter that
only counts actual `if` statement headers from the decompiled output; for
example compute header_count = sum(1 for line in out.splitlines() if
line.lstrip().startswith("if ")) and assert header_count == expected instead of
using out.count("if "). Ensure you replace each self.assertEqual(out.count("if
"), N) (or similar) with the header_count-based assertion using the same
expected N.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pycrefine.py`:
- Line 961: The loop currently binds unused variables k and jump_idx in "for k,
(jump_idx, jinstr, expr_instrs, _) in enumerate(group):"; remove the enumerate
and replace with a tuple unpack that discards the first element so it reads "for
_, jinstr, expr_instrs, _ in group:" (or, if you still need the index, keep
enumerate but use "_" for k and jump_idx: "for _, (_, jinstr, expr_instrs, _) in
enumerate(group):") to silence linters and remove the unused bindings while
keeping jinstr and expr_instrs intact.
- Around line 1049-1054: The current parentheses check uses "(" not in
res/next_expr which fails for function calls; change the assembly to propagate
explicit top-level-or metadata instead of string-scanning—introduce and use
boolean flags (e.g., res_has_top_level_or, next_has_top_level_or) alongside the
expression strings created earlier, and replace the two checks in the block that
inspects conn == "and" (where res and next_expr are combined) to wrap with
parentheses only when the corresponding top-level-or flag is True; update the
code paths that build res and next_expr to set these flags when they contain a
top-level "or" so precedence is preserved even if function calls include
parentheses.

In `@tests/test_pycrefine.py`:
- Around line 2836-2839: The test's allowed decompilation variants include an
incorrect boolean grouping that changes semantics; in the assertion block
checking output strings (the four ORed literals) remove the variant "((x is not
None and x > 0) or y is None) and z == 1" and only allow forms that are
logically equivalent to (x is not None and x > 0) or (y is None and z == 1) —
e.g., keep the original unparenthesized form and variants that parenthesize each
conjunction together like "(x is not None and x > 0) or (y is None and z == 1)"
or "(x is not None and x > 0) or y is None and z == 1", but do not accept any
grouping where the final "and z == 1" binds outside the intended right-hand
conjunction.
- Around line 2824-2826: The test currently enforces only the unparenthesised
form via assert_contains(out, "if x is None or y is None:") which contradicts
the comment; update the assertion to accept either "if x is None or y is None:"
or "if (x is None or y is None):" (for example by replacing the single
assert_contains call with a check that either string is present or a regex
match), and keep the existing self.assertEqual(out.count("if "), 1) to ensure
exactly one if-statement is emitted.

---

Duplicate comments:
In `@pycrefine.py`:
- Around line 982-984: The assignment of is_or_jump currently uses
is_success_type as a fallback which misclassifies intermediate jump targets;
change the logic so is_or_jump is computed from the subgroup/target boundary
information instead: inspect the current subgroup structure and the jump
target's boundary (whether the target is an intermediate node within the same
subgroup vs a final exit) and set is_or_jump true only when the subgroup/target
relation indicates an OR-style short-circuit jump, otherwise false; replace the
fallback use of is_success_type at the is_or_jump assignment with this
boundary/subgroup-derived check so mixed None-chains are handled correctly.

---

Nitpick comments:
In `@pycrefine.py`:
- Around line 2289-2304: Remove the unreachable redundant branch that handles
POP_JUMP_IF_NONE / POP_JUMP_IF_NOT_NONE: delete the entire elif block that
checks '"POP_JUMP_IF_NONE" in opname or "POP_JUMP_IF_NOT_NONE" in opname' (the
block that pops self.stack, sets is_not_none, calls self._append_reconstructed,
increments self.indent_level, computes jump_target via
self._get_jump_target(instr), and appends to self.blocks) because those opnames
are already handled earlier; ensure no other code paths rely on this block
before removing it.

In `@tests/test_pycrefine.py`:
- Around line 2800-2864: Replace fragile uses of out.count("if ") in the test
methods (e.g. test_compound_or, test_compound_mixed_and_or,
test_compound_none_and, test_compound_none_or, test_compound_complex_mixed,
test_compound_short_circuit_with_call, test_compound_nested_if_merge_regression)
with a line-based header counter that only counts actual `if` statement headers
from the decompiled output; for example compute header_count = sum(1 for line in
out.splitlines() if line.lstrip().startswith("if ")) and assert header_count ==
expected instead of using out.count("if "). Ensure you replace each
self.assertEqual(out.count("if "), N) (or similar) with the header_count-based
assertion using the same expected N.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 171e97cc-6e5e-4175-bcc2-2cf868e18d6c

📥 Commits

Reviewing files that changed from the base of the PR and between 57f81ca and 6faa48f.

📒 Files selected for processing (2)
  • pycrefine.py
  • tests/test_pycrefine.py

Comment thread pycrefine.py Outdated
Comment thread pycrefine.py Outdated
Comment thread tests/test_pycrefine.py Outdated
Comment thread tests/test_pycrefine.py Outdated
…t files for various language constructs and updates to the decompiler.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
pycrefine.py (1)

2166-2171: ⚠️ Potential issue | 🔴 Critical

Please unify conditional jump matching with _is_compound_cjump to avoid missing Python 3.11+ opnames.

Dear friend, this branch currently checks only POP_JUMP_IF_* shapes. POP_JUMP_FORWARD_IF_* / POP_JUMP_BACKWARD_IF_* can slip past it, so modern bytecode may skip if/while emission and compound-condition handling.

Suggested fix
-        elif (
-            "POP_JUMP_IF_FALSE" in opname
-            or "POP_JUMP_IF_TRUE" in opname
-            or "POP_JUMP_IF_NONE" in opname
-            or "POP_JUMP_IF_NOT_NONE" in opname
-        ):
+        elif self._is_compound_cjump(opname):
In CPython 3.11, 3.12, 3.13, and 3.14, what are the `dis` opcode names for conditional POP jumps (`IF_TRUE`, `IF_FALSE`, `IF_NONE`, `IF_NOT_NONE`)? Do they appear as `POP_JUMP_FORWARD_IF_*` / `POP_JUMP_BACKWARD_IF_*` rather than only `POP_JUMP_IF_*`?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pycrefine.py` around lines 2166 - 2171, Replace the ad-hoc opname checks for
conditional POP jumps with the shared matcher `_is_compound_cjump` so
forward/backward variants aren't missed; specifically, in the branch that
currently quizzes `opname` for `"POP_JUMP_IF_FALSE" / "POP_JUMP_IF_TRUE" /
"POP_JUMP_IF_NONE" / "POP_JUMP_IF_NOT_NONE"`, call `_is_compound_cjump(opname)`
(or reuse its internal pattern) instead of the manual string checks so
`POP_JUMP_FORWARD_IF_*` and `POP_JUMP_BACKWARD_IF_*` opnames from Python 3.11+
are handled consistently. Ensure you reference the existing `opname` variable
and remove the duplicated string-list checks.
🧹 Nitpick comments (1)
pycrefine.py (1)

1086-1093: Consider removing or wiring _is_compound_or_jump to active logic.

Dear friend, this helper appears unused. Keeping unused branch-logic helpers in decompiler control flow increases drift risk; either use it in _prescan_compound_conds() or delete it for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pycrefine.py` around lines 1086 - 1093, The helper function
_is_compound_or_jump is currently unused and should be either removed or wired
into the prescan routine: either delete the _is_compound_or_jump definition to
avoid dead code, or update _prescan_compound_conds to call
_is_compound_or_jump(opname) where you currently check opcode names (replace any
ad-hoc string checks like "IF_TRUE"/"IF_NONE" and AND/NOT exclusions with this
helper) so the helper is actually exercised; ensure tests/flows that depend on
_prescan_compound_conds still behave the same after the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@pycrefine.py`:
- Around line 2166-2171: Replace the ad-hoc opname checks for conditional POP
jumps with the shared matcher `_is_compound_cjump` so forward/backward variants
aren't missed; specifically, in the branch that currently quizzes `opname` for
`"POP_JUMP_IF_FALSE" / "POP_JUMP_IF_TRUE" / "POP_JUMP_IF_NONE" /
"POP_JUMP_IF_NOT_NONE"`, call `_is_compound_cjump(opname)` (or reuse its
internal pattern) instead of the manual string checks so `POP_JUMP_FORWARD_IF_*`
and `POP_JUMP_BACKWARD_IF_*` opnames from Python 3.11+ are handled consistently.
Ensure you reference the existing `opname` variable and remove the duplicated
string-list checks.

---

Nitpick comments:
In `@pycrefine.py`:
- Around line 1086-1093: The helper function _is_compound_or_jump is currently
unused and should be either removed or wired into the prescan routine: either
delete the _is_compound_or_jump definition to avoid dead code, or update
_prescan_compound_conds to call _is_compound_or_jump(opname) where you currently
check opcode names (replace any ad-hoc string checks like "IF_TRUE"/"IF_NONE"
and AND/NOT exclusions with this helper) so the helper is actually exercised;
ensure tests/flows that depend on _prescan_compound_conds still behave the same
after the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6d00932-eaf8-4636-b548-f857b9543922

📥 Commits

Reviewing files that changed from the base of the PR and between 57f81ca and 17a6c40.

📒 Files selected for processing (2)
  • pycrefine.py
  • tests/test_pycrefine.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_pycrefine.py

…of operator precedence, binary operations, and conditional jumps.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compound if statements are incorrectly placed in multiple lines

1 participant