Compiler parity: docstring dedent, StopIteration wrapper, constant folding#7530
Compiler parity: docstring dedent, StopIteration wrapper, constant folding#7530youknowone wants to merge 2 commits intoRustPython:mainfrom
Conversation
…lding - Fix clean_doc to skip first line in margin calculation (match CPython) - Add PEP 479 StopIteration handler to generator/coroutine bodies (SETUP_CLEANUP + CALL_INTRINSIC_1(StopIterationError) + RERAISE) - Remove yield-from/await -1 depth compensation in RESUME DEPTH1 - Add constant condition elimination for if/while (if False, while True) - Add POP_JUMP_IF_NONE/NOT_NONE for `x is None` patterns - Add binary op constant folding (*, **, +, -, <<, >>, etc.) - Add expr_constant() for compile-time boolean evaluation
📝 WalkthroughWalkthroughThis pull request introduces compile-time constant detection and folding optimizations across the compiler pipeline. Changes include dead-code elimination for conditionals with constant-boolean tests, a new binary operation constant-folding pass in IR, improved exception handling for generators/coroutines, and specialized jump instructions for None comparisons. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Walk dead branches to consume sub_tables without emitting bytecode, matching CPython's BEGIN_DO_NOT_EMIT_BYTECODE pattern. Fixes freeze compilation errors for files containing scope-creating definitions inside dead branches (e.g. if False: def foo(): ...).
19d8a3d to
15e2b2e
Compare
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/codegen/src/compile.rs`:
- Around line 8737-8740: The flag do_not_emit_bytecode only prevents emitting
instructions in _emit(), but helpers like emit_load_const call arg_constant()
(which interns constants) before _emit(), leaking metadata; modify emit helpers
(e.g., emit_load_const) and/or arg_constant() to check do_not_emit_bytecode and
skip interning when do_not_emit_bytecode > 0 so no constants/names are added
while bytecode emission is disabled, ensuring _emit, emit_load_const,
arg_constant (and any other helpers that mutate constant/name tables) return
early or no-op metadata mutations when the flag is set.
- Around line 3871-3885: The PEP 479 StopIteration wrapper currently wraps only
compile_function_body() and omits generator/coroutine code objects created in
compile_comprehension(), causing StopIteration to escape; modify
compile_comprehension() to mirror the same logic used where is_gen is computed
(create handler_block via new_block(), emit PseudoInstruction::SetupCleanup {
delta: handler_block }, and call push_fblock(FBlockType::StopIteration,
handler_block, handler_block) when the comprehension builds a
generator/coroutine) or refactor the wrapper into a small helper used by both
compile_function_body() and compile_comprehension(); also apply the same fix to
the duplicate site referenced around the other block (the section analogous to
3917-3929).
- Around line 8943-8963: The current expr_constant implementation incorrectly
returns Some(!elts.is_empty()) for ast::Expr::Tuple, which folds away element
evaluation (and side effects) for non-empty tuples; change this to only return
Some(false) for an empty tuple and for non-empty tuples return Some(true) only
if every element is a compile-time constant (call expr_constant recursively on
each element and if any returns None, return None), otherwise return None so
elements with possible side effects are preserved; update the ast::Expr::Tuple
branch in expr_constant accordingly (check elts, recurse into each elt, and only
yield Some(true) when all elts are constant).
- Around line 5217-5224: The current early-return for constant == Some(true)
skips compiling the elif/else tests, which bypasses async-await validation and
leaves nested scope sub_tables unconsumed; instead, keep the no-emission mode
but still compile each clause's test expressions (and any nested
lambdas/comprehensions) so errors and scope consumption occur. Concretely,
inside the constant == Some(true) branch where you call self.emit_nop() and
self.compile_statements(body) and adjust self.do_not_emit_bytecode, also iterate
over elif_else_clauses and call the same compiler path that validates/compiles
their test expressions (e.g. use the existing expression-compile routine used
for condition checks) with do_not_emit_bytecode incremented so no bytecode is
emitted but await-checks and sub_table consumption run; do not compile clause
bodies, only their tests/scopes. Ensure you reference self.compile_statements,
elif_else_clauses, and self.do_not_emit_bytecode when making the change.
- Around line 8992-8994: The early return when self.do_not_emit_bytecode > 0
skips loop/except* validation and lets dead branches (e.g., `if False: break`)
pass; instead, preserve the validation logic and only skip emission. Change the
code around the `if self.do_not_emit_bytecode > 0 { return Ok(()); }` guard so
that you still execute the loop/except* checks that follow (the
dead-branch/`break`/`continue` and `except*` validation code) but bypass only
the bytecode emission paths—e.g., wrap emission-specific calls with a check `if
self.do_not_emit_bytecode == 0 { ... }` or move the `return` to after the
validation block; keep references to `self.do_not_emit_bytecode`, the
loop/except* validation code paths, and emission sites to locate and update the
necessary conditionals.
In `@crates/codegen/src/ir.rs`:
- Around line 193-196: The binop-constant folding pass should run again after
UNARY_NEGATIVE has been folded into LOAD_CONST so expressions like `-1 + 2` and
`2 * -3` are caught; update the sequence around fold_binop_constants(),
fold_unary_negative(), and remove_nops() to call fold_binop_constants() once
more immediately after fold_unary_negative() (keeping the surrounding
remove_nops() calls as needed) so binary constant folding re-runs on the newly
produced constants.
- Around line 787-798: The BinOp::FloorDivide and BinOp::Remainder folding
currently uses Rust’s / and % which produce non-Python semantics for BigInt and
floats; change the BigInt cases to call malachite_bigint::BigInt methods
.div_floor() and .mod_floor() (used by the runtime) instead of l / r and l % r,
and for the float remainder branch replicate the Python floor-mod behavior by
applying the sign-correction logic from float_ops::mod_() (or the divmod helper)
so result sign follows the divisor (i.e., matches Python’s // and % semantics
for negative values).
🪄 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: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: e2c39f50-5b6c-4884-a8a3-1bc68e3aad8f
⛔ Files ignored due to path filters (1)
crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snapis excluded by!**/*.snap
📒 Files selected for processing (2)
crates/codegen/src/compile.rscrates/codegen/src/ir.rs
| // PEP 479: Wrap generator/coroutine body with StopIteration handler | ||
| let is_gen = is_async || self.current_symbol_table().is_generator; | ||
| let stop_iteration_block = if is_gen { | ||
| let handler_block = self.new_block(); | ||
| emit!( | ||
| self, | ||
| PseudoInstruction::SetupCleanup { | ||
| delta: handler_block | ||
| } | ||
| ); | ||
| self.push_fblock(FBlockType::StopIteration, handler_block, handler_block)?; | ||
| Some(handler_block) | ||
| } else { | ||
| None | ||
| }; |
There was a problem hiding this comment.
PEP 479 still bypasses comprehension-generated code objects.
This wrapper only covers compile_function_body(). Generator expressions and non-inlined async comprehensions build their own generator/coroutine code objects in compile_comprehension() around Line 8231, so StopIteration escaping from those bodies will still miss StopIterationError.
Also applies to: 3917-3929
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/codegen/src/compile.rs` around lines 3871 - 3885, The PEP 479
StopIteration wrapper currently wraps only compile_function_body() and omits
generator/coroutine code objects created in compile_comprehension(), causing
StopIteration to escape; modify compile_comprehension() to mirror the same logic
used where is_gen is computed (create handler_block via new_block(), emit
PseudoInstruction::SetupCleanup { delta: handler_block }, and call
push_fblock(FBlockType::StopIteration, handler_block, handler_block) when the
comprehension builds a generator/coroutine) or refactor the wrapper into a small
helper used by both compile_function_body() and compile_comprehension(); also
apply the same fix to the duplicate site referenced around the other block (the
section analogous to 3917-3929).
| if constant == Some(true) { | ||
| self.emit_nop(); | ||
| self.compile_statements(body)?; | ||
| self.do_not_emit_bytecode += 1; | ||
| for clause in elif_else_clauses { | ||
| self.compile_statements(&clause.body)?; | ||
| } | ||
| self.do_not_emit_bytecode -= 1; |
There was a problem hiding this comment.
Dead elif tests still need validation and scope consumption.
The constant-true path skips those tests entirely. if True: pass elif await x: ... stops triggering the await-outside-async error, and lambdas/comprehensions inside the skipped tests leave their sub_tables unconsumed for the next live nested scope.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/codegen/src/compile.rs` around lines 5217 - 5224, The current
early-return for constant == Some(true) skips compiling the elif/else tests,
which bypasses async-await validation and leaves nested scope sub_tables
unconsumed; instead, keep the no-emission mode but still compile each clause's
test expressions (and any nested lambdas/comprehensions) so errors and scope
consumption occur. Concretely, inside the constant == Some(true) branch where
you call self.emit_nop() and self.compile_statements(body) and adjust
self.do_not_emit_bytecode, also iterate over elif_else_clauses and call the same
compiler path that validates/compiles their test expressions (e.g. use the
existing expression-compile routine used for condition checks) with
do_not_emit_bytecode incremented so no bytecode is emitted but await-checks and
sub_table consumption run; do not compile clause bodies, only their
tests/scopes. Ensure you reference self.compile_statements, elif_else_clauses,
and self.do_not_emit_bytecode when making the change.
| fn _emit<I: Into<AnyInstruction>>(&mut self, instr: I, arg: OpArg, target: BlockIdx) { | ||
| if self.do_not_emit_bytecode > 0 { | ||
| return; | ||
| } |
There was a problem hiding this comment.
do_not_emit_bytecode still leaks dead metadata into the live code object.
Only _emit() is gated here. Helpers like emit_load_const() still intern via arg_constant() at Line 8847 before _emit() runs, so dead branches can still populate the constant/name tables even when no bytecode is emitted.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/codegen/src/compile.rs` around lines 8737 - 8740, The flag
do_not_emit_bytecode only prevents emitting instructions in _emit(), but helpers
like emit_load_const call arg_constant() (which interns constants) before
_emit(), leaking metadata; modify emit helpers (e.g., emit_load_const) and/or
arg_constant() to check do_not_emit_bytecode and skip interning when
do_not_emit_bytecode > 0 so no constants/names are added while bytecode emission
is disabled, ensuring _emit, emit_load_const, arg_constant (and any other
helpers that mutate constant/name tables) return early or no-op metadata
mutations when the flag is set.
| fn expr_constant(expr: &ast::Expr) -> Option<bool> { | ||
| match expr { | ||
| ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) => Some(*value), | ||
| ast::Expr::NoneLiteral(_) => Some(false), | ||
| ast::Expr::EllipsisLiteral(_) => Some(true), | ||
| ast::Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => match value { | ||
| ast::Number::Int(i) => { | ||
| let n: i64 = i.as_i64().unwrap_or(1); | ||
| Some(n != 0) | ||
| } | ||
| ast::Number::Float(f) => Some(*f != 0.0), | ||
| ast::Number::Complex { real, imag, .. } => Some(*real != 0.0 || *imag != 0.0), | ||
| }, | ||
| ast::Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => { | ||
| Some(!value.to_str().is_empty()) | ||
| } | ||
| ast::Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => { | ||
| Some(value.bytes().next().is_some()) | ||
| } | ||
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => Some(!elts.is_empty()), | ||
| _ => None, |
There was a problem hiding this comment.
Non-empty tuples are not unconditional compile-time true.
Some(!elts.is_empty()) folds if (side_effect(),): to an unconditional branch and drops the tuple element evaluation. This needs a recursive side-effect-free constant check, or None for non-empty tuples whose elements are not all constants.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/codegen/src/compile.rs` around lines 8943 - 8963, The current
expr_constant implementation incorrectly returns Some(!elts.is_empty()) for
ast::Expr::Tuple, which folds away element evaluation (and side effects) for
non-empty tuples; change this to only return Some(false) for an empty tuple and
for non-empty tuples return Some(true) only if every element is a compile-time
constant (call expr_constant recursively on each element and if any returns
None, return None), otherwise return None so elements with possible side effects
are preserved; update the ast::Expr::Tuple branch in expr_constant accordingly
(check elts, recurse into each elt, and only yield Some(true) when all elts are
constant).
| if self.do_not_emit_bytecode > 0 { | ||
| return Ok(()); | ||
| } |
There was a problem hiding this comment.
Keep break/continue validation even when bytecode is suppressed.
This returns before the loop and except* checks below, so dead branches like if False: break no longer raise SyntaxError. Only the cleanup/jump emission should be skipped here.
🔧 Minimal fix
- if self.do_not_emit_bytecode > 0 {
- return Ok(());
- }
-
// unwind_fblock_stack
// We need to unwind fblocks and compile cleanup code. For FinallyTry blocks,
// we need to compile the finally body inline, but we must temporarily pop
@@
let Some(loop_idx) = loop_idx else {
if is_break {
return Err(self.error_ranged(CodegenErrorType::InvalidBreak, range));
} else {
return Err(self.error_ranged(CodegenErrorType::InvalidContinue, range));
}
};
+
+ if self.do_not_emit_bytecode > 0 {
+ return Ok(());
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/codegen/src/compile.rs` around lines 8992 - 8994, The early return
when self.do_not_emit_bytecode > 0 skips loop/except* validation and lets dead
branches (e.g., `if False: break`) pass; instead, preserve the validation logic
and only skip emission. Change the code around the `if self.do_not_emit_bytecode
> 0 { return Ok(()); }` guard so that you still execute the loop/except* checks
that follow (the dead-branch/`break`/`continue` and `except*` validation code)
but bypass only the bytecode emission paths—e.g., wrap emission-specific calls
with a check `if self.do_not_emit_bytecode == 0 { ... }` or move the `return` to
after the validation block; keep references to `self.do_not_emit_bytecode`, the
loop/except* validation code paths, and emission sites to locate and update the
necessary conditionals.
| self.fold_binop_constants(); | ||
| self.remove_nops(); | ||
| self.fold_unary_negative(); | ||
| self.remove_nops(); // remove NOPs from unary folding so tuple/list/set see contiguous LOADs |
There was a problem hiding this comment.
Run binary folding once more after unary-negative folding.
With the current sequence, -1 + 2 and 2 * -3 never reach fold_binop_constants(): the binop pass runs before fold_unary_negative(), and there is no second binop pass once UNARY_NEGATIVE has collapsed into LOAD_CONST. Re-running the binop fold after Line 195 closes that parity hole.
Suggested change
self.fold_binop_constants();
self.remove_nops();
self.fold_unary_negative();
self.remove_nops(); // remove NOPs from unary folding so tuple/list/set see contiguous LOADs
+ self.fold_binop_constants();
+ self.remove_nops();
self.fold_tuple_constants();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| self.fold_binop_constants(); | |
| self.remove_nops(); | |
| self.fold_unary_negative(); | |
| self.remove_nops(); // remove NOPs from unary folding so tuple/list/set see contiguous LOADs | |
| self.fold_binop_constants(); | |
| self.remove_nops(); | |
| self.fold_unary_negative(); | |
| self.remove_nops(); // remove NOPs from unary folding so tuple/list/set see contiguous LOADs | |
| self.fold_binop_constants(); | |
| self.remove_nops(); | |
| self.fold_tuple_constants(); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/codegen/src/ir.rs` around lines 193 - 196, The binop-constant folding
pass should run again after UNARY_NEGATIVE has been folded into LOAD_CONST so
expressions like `-1 + 2` and `2 * -3` are caught; update the sequence around
fold_binop_constants(), fold_unary_negative(), and remove_nops() to call
fold_binop_constants() once more immediately after fold_unary_negative()
(keeping the surrounding remove_nops() calls as needed) so binary constant
folding re-runs on the newly produced constants.
| BinOp::FloorDivide => { | ||
| if r.is_zero() { | ||
| return None; | ||
| } | ||
| l / r | ||
| } | ||
| BinOp::Remainder => { | ||
| if r.is_zero() { | ||
| return None; | ||
| } | ||
| l % r | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For negative operands, what are the semantics of malachite_bigint::BigInt /versus num_integer::Integer::div_floor/mod_floor, what are Rustf64 % f64semantics, and what do Python//and% require?
💡 Result:
For negative operands: malachite_bigint::BigInt / : No explicit documentation found on division semantics for negative numbers in official docs.rs pages. Malachite provides multiple division traits like FloorDiv, CeilingDiv, DivRound (with rounding modes), but the / operator semantics are not specified in searched sources. As a drop-in replacement for num-bigint, it may follow similar conventions, but unconfirmed. Likely towards zero or floored based on GMP inspiration, but evidence insufficient (low confidence). num_integer::Integer::div_floor/mod_floor: Uses floored division (round quotient towards -∞). Examples: 8/3=2 r=2; 8/-3=-3 r=-1; -8/3=-3 r=1; -8/-3=2 r=-2. Satisfies q*d + r = n with sign(r) matching d. Rust f64 % f64: The % operator is truncating remainder (towards zero division), sign follows dividend. For Euclidean (always non-negative remainder), use rem_euclid (pairs with div_euclid, 0 <= r < |d| approximately). Python // and % : // is floor division (towards -∞); % is remainder with sign of divisor, |r| < |d|, satisfying n = (n//d)*d + (n%d). Examples: (-8)//3=-3, (-8)%3=1; 8//-3=-3, 8%-3=-1.
Citations:
- 1: https://docs.rs/num-integer/latest/num_integer/trait.Integer.html
- 2: https://docs.rs/malachite-nz/latest/malachite_nz/integer/struct.Integer.html
- 3: https://doc.rust-lang.org/std/primitive.f64.html
- 4: https://docs.python.org/3/reference/expressions.html?highlight=operators
- 5: https://docs.python.org/3/library/stdtypes.html
🏁 Script executed:
# First, let's see the file size and check the lines mentioned
wc -l crates/codegen/src/ir.rsRepository: RustPython/RustPython
Length of output: 94
🏁 Script executed:
# Now read the specific lines mentioned (787-798 and 841-846) with context
sed -n '780,850p' crates/codegen/src/ir.rs | cat -nRepository: RustPython/RustPython
Length of output: 3340
🏁 Script executed:
# Check imports and early context to understand what types are being used
head -100 crates/codegen/src/ir.rs | cat -nRepository: RustPython/RustPython
Length of output: 3677
🏁 Script executed:
# Search for malachite_bigint usage and available division methods
rg "div_floor|mod_floor|FloorDiv|floor_div" crates/codegen/src/ir.rsRepository: RustPython/RustPython
Length of output: 152
🏁 Script executed:
# Check if there are any utility functions for Python-compatible division
rg "def.*floor.*div|fn.*floor.*div" crates/Repository: RustPython/RustPython
Length of output: 723
🏁 Script executed:
# Look at the full context around the problem lines
sed -n '770,860p' crates/codegen/src/ir.rs | cat -nRepository: RustPython/RustPython
Length of output: 4130
🏁 Script executed:
# Check what malachite_bigint methods are available
web_search
Malachite BigInt division operators - does / operator use floor division or truncating division semantics?Repository: RustPython/RustPython
Length of output: 163
🏁 Script executed:
# Check if there's a common float_ops or int_ops module with Python-compatible helpers
cat crates/common/src/float_ops.rs | head -50Repository: RustPython/RustPython
Length of output: 1688
🏁 Script executed:
# Search for how BigInt division is actually implemented in runtime
cat crates/vm/src/builtins/int.rs | grep -A 20 "inner_floordiv"Repository: RustPython/RustPython
Length of output: 1497
🏁 Script executed:
# Check if malachite_bigint::BigInt has a / operator and what it does
rg "impl.*Div.*for.*BigInt" --type rustRepository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
# Look at what remainder methods exist for BigInt
rg "mod_floor|rem|Rem" crates/vm/src/builtins/int.rs -A 3 -B 1Repository: RustPython/RustPython
Length of output: 995
🏁 Script executed:
# Check if there are Python-compatible modulo implementations for f64
cat crates/common/src/float_ops.rs | grep -A 10 "floordiv\|modulo\|remainder"Repository: RustPython/RustPython
Length of output: 305
🏁 Script executed:
# Search for actual usage of % operator with BigInt to understand current behavior
rg "BigInt.*%|% r" crates/vm/src/builtins/ -A 2 -B 2Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
# Check the full divmod function for f64 to understand Python-compatible modulo
cat crates/common/src/float_ops.rs | grep -A 20 "pub fn divmod"Repository: RustPython/RustPython
Length of output: 607
🏁 Script executed:
# Check what modulo operations are available in float_ops
cat crates/common/src/float_ops.rsRepository: RustPython/RustPython
Length of output: 7815
🏁 Script executed:
# Verify if there's a modulo helper for f64 that computes Python-style remainder
rg "def.*mod|fn.*mod" crates/common/src/float_ops.rsRepository: RustPython/RustPython
Length of output: 167
🏁 Script executed:
# See how runtime float remainder is implemented
rg "inner_mod\|BinOp::Remainder" crates/vm/src/builtins/float.rs -A 10 -B 2Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
# Check if malachite_bigint::BigInt actually has div_floor and mod_floor methods
rg "div_floor\|mod_floor" crates/ -t rust | head -20Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
# Verify the exact signature and availability of these methods
rg "\.div_floor\(|\.mod_floor\(" crates/vm/src/builtins/int.rs -B 2 -A 2Repository: RustPython/RustPython
Length of output: 341
🏁 Script executed:
# Check if there's a way to import float_ops helpers in the codegen file
head -30 crates/codegen/src/ir.rs | grep "use\|import"Repository: RustPython/RustPython
Length of output: 267
Use Python-compatible // and % operators instead of raw Rust operators.
These arms currently fold with Rust-native division/remainder behavior. For BigInt operations (lines 787–798), use .div_floor() and .mod_floor() instead of / and %—these methods are available on malachite_bigint::BigInt and are already used by the runtime (crates/vm/src/builtins/int.rs). For float modulo (line 841–846), the raw % operator applies truncating remainder with the dividend's sign, but Python requires floor modulo with the divisor's sign. Apply the sign-correction logic from float_ops::mod_() (in crates/common/src/float_ops.rs) or replicate the divmod helper's approach.
Negative cases like -3 // 2, -3 % 2, or -1.0 % 3.0 will fold to wrong constants without these fixes.
Also applies to: 841–846
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/codegen/src/ir.rs` around lines 787 - 798, The BinOp::FloorDivide and
BinOp::Remainder folding currently uses Rust’s / and % which produce non-Python
semantics for BigInt and floats; change the BigInt cases to call
malachite_bigint::BigInt methods .div_floor() and .mod_floor() (used by the
runtime) instead of l / r and l % r, and for the float remainder branch
replicate the Python floor-mod behavior by applying the sign-correction logic
from float_ops::mod_() (or the divmod helper) so result sign follows the divisor
(i.e., matches Python’s // and % semantics for negative values).
x is NonepatternsSummary by CodeRabbit