Skip to content

re.fullmatch with possessive_repeat patterns doesn't behave as expected #7183

@wvmscs

Description

@wvmscs

Feature

for example:

re.fullmatch("([0-9]++(?:\\.[0-9]+)*+)", "10.20.3") is expected to match, and does with CPython 3.14.3 ,
return None whith Python 3.14.0alpha.

(heads/main:7f64acdd5, Feb 12 2026, 16:19:33)
[RustPython 0.4.0 with rustc 1.93.0 (254b59607 2026-01-19)]

Error in source code

The bug is in the crate sre_engine where the context.toplevel must be set to false after processing the opcodes POSSESSIVE_REPEAT and ATOMIC_GROUP

Corrections made

I would submit the correction in the proper form in a few hours.
here is a preview.

diff --git a/crates/sre_engine/src/engine.rs b/crates/sre_engine/src/engine.rs

index 36a7a9d48..2bdcaa2a3 100644
--- a/crates/sre_engine/src/engine.rs
+++ b/crates/sre_engine/src/engine.rs
@@ -469,8 +469,10 @@ fn _match<S: StrDrive>(req: &Request<'_, S>, state: &mut State, mut ctx: MatchCo
                     }
                     Jump::PossessiveRepeat1 => {
                         let min_count = ctx.peek_code(req, 2) as isize;
-                        if ctx.count < min_count {
-                            break 'context ctx.next_offset(4, Jump::PossessiveRepeat2);
+                        if ctx.count < min_count { // modified next.toplevel from herited to false
+                            let mut next = ctx.next_offset(4, Jump::PossessiveRepeat2);
+                            next.toplevel = false;
+                            break 'context next;
                         }
                         // zero match protection
                         ctx.cursor.position = usize::MAX;
@@ -494,7 +496,9 @@ fn _match<S: StrDrive>(req: &Request<'_, S>, state: &mut State, mut ctx: MatchCo
                         {
                             state.marks.push();
                             ctx.cursor = state.cursor;
-                            break 'context ctx.next_offset(4, Jump::PossessiveRepeat4);
+                            let mut next = ctx.next_offset(4, Jump::PossessiveRepeat4);
+                            next.toplevel = false; // modified next.toplevel from herited to false
+                            break 'context next;
                         }
                         ctx.cursor = state.cursor;
                         ctx.skip_code_from(req, 1);
@@ -832,7 +836,9 @@ fn _match<S: StrDrive>(req: &Request<'_, S>, state: &mut State, mut ctx: MatchCo
                         /* <ATOMIC_GROUP> <skip> pattern <SUCCESS> tail */
                         SreOpcode::ATOMIC_GROUP => {
                             state.cursor = ctx.cursor;
-                            break 'context ctx.next_offset(2, Jump::AtomicGroup1);
+                            let mut next_ctx = ctx.next_offset(2, Jump::AtomicGroup1);
+                            next_ctx.toplevel = false;  // modified next.toplevel from herited to false
+                            break 'context next_ctx;
                         }
                         /* <POSSESSIVE_REPEAT> <skip> <1=min> <2=max> pattern
                         <SUCCESS> tail */


diff --git a/crates/sre_engine/tests/tests.rs b/crates/sre_engine/tests/tests.rs
index 72a11c55b..fc5c14091 100644
--- a/crates/sre_engine/tests/tests.rs
+++ b/crates/sre_engine/tests/tests.rs
@@ -145,6 +145,18 @@ fn test_possessive_quantifier() {
     assert!(state.py_match(&req));
 }
 
+#[test]
+fn test_possessive_repeat_fullmatch() {
+    // pattern p = re.compile("([0-9]++(?:\.[0-9]+)*+)", re.I   )
+    // [INFO, 4, 0, 1, 4294967295, MARK, 0, POSSESSIVE_REPEAT_ONE, 10, 1, MAXREPEAT, IN, 5, RANGE, 48, 57, FAILURE, SUCCESS, POSSESSIVE_REPEAT, 16, 0, MAXREPEAT, LITERAL, 46, REPEAT_ONE, 10, 1, MAXREPEAT, IN, 5, RANGE, 48, 57, FAILURE, SUCCESS, SUCCESS, MARK, 1, SUCCESS]
+    // START GENERATED by generate_tests.py
+    #[rustfmt::skip] let p = Pattern { pattern: "([0-9]++(?:\\.[0-9]+)*+)", code: &[14, 4, 0, 1, 4294967295, 17, 0, 29, 10, 1, 4294967295, 13, 5, 22, 48, 57, 0, 1, 28, 16, 0, 4294967295, 16, 46, 24, 10, 1, 4294967295, 13, 5, 22, 48, 57, 0, 1, 1, 17, 1, 1] };       
+    // END GENERATED
+    let (mut req, mut state) = p.state("1.25.38");
+    req.match_all = true;
+    assert!(state.py_match(&req), "should match");
+}
+
 #[test]
 fn test_possessive_atomic_group() {
     // pattern p = re.compile('(?>x)++x')

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-compatA discrepancy between RustPython and CPython

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions