@@ -301,6 +301,26 @@ pub const BacktrackEngine = struct {
301301
302302 @memcpy (self .captures , saved_captures );
303303 }
304+ if (! right_has_choices and self .needsAnchoredRepeatedAlternationRecovery (concat .left , concat .right )) {
305+ var target = self .input .len ;
306+ while (target >= pos ) {
307+ @memcpy (self .captures , clean_captures );
308+ const screened = self .matchNode (concat .right , target ) orelse {
309+ if (target == pos ) break ;
310+ target -= 1 ;
311+ continue ;
312+ };
313+ @memcpy (self .captures , clean_captures );
314+ if (self .matchNodeConstrained (concat .left , pos , target )) {
315+ @memcpy (self .captures , clean_captures );
316+ _ = self .matchNodeConstrained (concat .left , pos , target );
317+ if (self .matchNode (concat .right , target )) | result | return result ;
318+ }
319+ _ = screened ;
320+ if (target == pos ) break ;
321+ target -= 1 ;
322+ }
323+ }
304324 @memcpy (self .captures , clean_captures );
305325 return null ;
306326 } else {
@@ -373,10 +393,36 @@ pub const BacktrackEngine = struct {
373393 if (right_end == target_end ) return true ;
374394 }
375395 }
396+ if (self .canMatchEmpty (c .right )) {
397+ @memcpy (self .captures , base_captures );
398+ if (self .matchNodeConstrained (c .left , pos , target_end ) and
399+ self .matchNodeConstrained (c .right , target_end , target_end )) return true ;
400+ }
376401 @memcpy (self .captures , base_captures );
377402 return false ;
378403 }
379404 },
405+ .star , .plus , .optional , .repeat = > {
406+ return self .matchQuantifierConstrained (node , pos , target_end );
407+ },
408+ .alternation = > {
409+ if (self .matchNodeConstrained (node .data .alternation .left , pos , target_end )) return true ;
410+ return self .matchNodeConstrained (node .data .alternation .right , pos , target_end );
411+ },
412+ .backref = > {
413+ return if (self .matchBackreference (node .data .backref , pos )) | end | end == target_end else false ;
414+ },
415+ .lookahead = > {
416+ return if (self .matchLookahead (node .data .lookahead , pos )) | end | end == target_end else false ;
417+ },
418+ .lookbehind = > {
419+ return if (self .matchLookbehind (node .data .lookbehind , pos )) | end | end == target_end else false ;
420+ },
421+ }
422+ }
423+
424+ fn matchQuantifierConstrained (self : * BacktrackEngine , node : * ast.Node , pos : usize , target_end : usize ) bool {
425+ switch (node .node_type ) {
380426 .star , .plus , .optional , .repeat = > {
381427 const child = switch (node .node_type ) {
382428 .star = > node .data .star .child ,
@@ -392,63 +438,58 @@ pub const BacktrackEngine = struct {
392438 else = > unreachable ,
393439 };
394440
395- // A required zero-width iteration, such as `(?=(x)){1,1}`,
396- // still runs the child and can update captures. Optional
397- // zero-width iterations are skipped, matching RepeatMatcher's
398- // min=0 empty-iteration failure rule.
399- if (pos == target_end ) {
400- var count : usize = 0 ;
401- while (count < min ) : (count += 1 ) {
402- if (! self .matchNodeConstrained (child , pos , target_end )) return false ;
403- }
404- return true ;
405- }
406-
407- // For optional, only one match allowed
408441 if (node .node_type == .optional ) {
409- return if (self .matchNode (child , pos )) | end | end == target_end else false ;
442+ if (pos == target_end ) return true ;
443+ return self .matchNodeConstrained (child , pos , target_end );
410444 }
411445
412- // Try matching child repeatedly until we reach target_end
413- var current = pos ;
414- var count : usize = 0 ;
415- while (current < target_end ) {
416- self .clearCapturesIn (child );
417- if (self .matchNode (child , current )) | next | {
418- if (next <= current ) return false ;
419- current = next ;
420- count += 1 ;
421- if (current == target_end ) {
422- // Validate count constraints
423- if (node .node_type == .plus and count < 1 ) return false ;
424- if (node .node_type == .repeat ) {
425- if (count < node .data .repeat .bounds .min ) return false ;
426- if (node .data .repeat .bounds .max ) | max | {
427- if (count > max ) return false ;
428- }
429- }
430- return true ;
431- }
432- } else {
433- return false ;
434- }
435- }
436- return false ;
437- },
438- .alternation = > {
439- if (self .matchNodeConstrained (node .data .alternation .left , pos , target_end )) return true ;
440- return self .matchNodeConstrained (node .data .alternation .right , pos , target_end );
441- },
442- .backref = > {
443- return if (self .matchBackreference (node .data .backref , pos )) | end | end == target_end else false ;
444- },
445- .lookahead = > {
446- return if (self .matchLookahead (node .data .lookahead , pos )) | end | end == target_end else false ;
447- },
448- .lookbehind = > {
449- return if (self .matchLookbehind (node .data .lookbehind , pos )) | end | end == target_end else false ;
446+ const max : ? usize = switch (node .node_type ) {
447+ .star , .plus = > null ,
448+ .repeat = > node .data .repeat .bounds .max ,
449+ else = > unreachable ,
450+ };
451+ return self .matchRepeatedChildConstrained (child , pos , target_end , 0 , min , max );
450452 },
453+ else = > return false ,
454+ }
455+ }
456+
457+ fn matchRepeatedChildConstrained (self : * BacktrackEngine , child : * ast.Node , pos : usize , target_end : usize , count : usize , min : usize , max : ? usize ) bool {
458+ if (count >= min and pos == target_end ) return true ;
459+ if (pos == target_end ) {
460+ var n = count ;
461+ while (n < min ) : (n += 1 ) {
462+ if (! self .matchNodeConstrained (child , pos , target_end )) return false ;
463+ }
464+ return true ;
451465 }
466+ if (pos >= target_end ) return false ;
467+ if (max ) | m | if (count >= m ) return false ;
468+
469+ const saved = self .allocator .alloc (CaptureGroup , self .captures .len ) catch return false ;
470+ defer self .allocator .free (saved );
471+ @memcpy (saved , self .captures );
472+
473+ var ends = std .ArrayList (usize ).initCapacity (self .allocator , 0 ) catch return false ;
474+ defer ends .deinit (self .allocator );
475+ self .collectAllMatches (child , pos , & ends ) catch return false ;
476+
477+ for (ends .items ) | end | {
478+ if (end <= pos or end > target_end ) continue ;
479+ @memcpy (self .captures , saved );
480+ if (! self .matchNodeConstrained (child , pos , end )) continue ;
481+ if (self .matchRepeatedChildConstrained (child , end , target_end , count + 1 , min , max )) return true ;
482+ }
483+
484+ if (self .hasAlternation (child ) and self .hasCapturingGroup (child )) {
485+ @memcpy (self .captures , saved );
486+ if (self .matchNodeConstrained (child , pos , target_end )) {
487+ if (self .matchRepeatedChildConstrained (child , target_end , target_end , count + 1 , min , max )) return true ;
488+ }
489+ }
490+
491+ @memcpy (self .captures , saved );
492+ return false ;
452493 }
453494
454495 fn hasQuantifiers (self : * BacktrackEngine , node : * ast.Node ) bool {
@@ -478,6 +519,49 @@ pub const BacktrackEngine = struct {
478519 };
479520 }
480521
522+ fn hasRepeatedAlternation (self : * BacktrackEngine , node : * ast.Node ) bool {
523+ return switch (node .node_type ) {
524+ .star = > self .hasAlternation (node .data .star .child ) or self .hasRepeatedAlternation (node .data .star .child ),
525+ .plus = > self .hasAlternation (node .data .plus .child ) or self .hasRepeatedAlternation (node .data .plus .child ),
526+ .repeat = > self .hasAlternation (node .data .repeat .child ) or self .hasRepeatedAlternation (node .data .repeat .child ),
527+ .optional = > self .hasRepeatedAlternation (node .data .optional .child ),
528+ .concat = > self .hasRepeatedAlternation (node .data .concat .left ) or self .hasRepeatedAlternation (node .data .concat .right ),
529+ .alternation = > self .hasRepeatedAlternation (node .data .alternation .left ) or self .hasRepeatedAlternation (node .data .alternation .right ),
530+ .group = > self .hasRepeatedAlternation (node .data .group .child ),
531+ .lookahead = > self .hasRepeatedAlternation (node .data .lookahead .child ),
532+ .lookbehind = > self .hasRepeatedAlternation (node .data .lookbehind .child ),
533+ else = > false ,
534+ };
535+ }
536+
537+ fn needsAnchoredRepeatedAlternationRecovery (self : * BacktrackEngine , left : * ast.Node , right : * ast.Node ) bool {
538+ return self .hasRepeatedAlternation (left ) and self .hasCapturingGroup (left ) and self .isEndAnchorOnly (right );
539+ }
540+
541+ fn hasCapturingGroup (self : * BacktrackEngine , node : * ast.Node ) bool {
542+ return switch (node .node_type ) {
543+ .group = > node .data .group .capture_index != null or self .hasCapturingGroup (node .data .group .child ),
544+ .concat = > self .hasCapturingGroup (node .data .concat .left ) or self .hasCapturingGroup (node .data .concat .right ),
545+ .alternation = > self .hasCapturingGroup (node .data .alternation .left ) or self .hasCapturingGroup (node .data .alternation .right ),
546+ .star = > self .hasCapturingGroup (node .data .star .child ),
547+ .plus = > self .hasCapturingGroup (node .data .plus .child ),
548+ .optional = > self .hasCapturingGroup (node .data .optional .child ),
549+ .repeat = > self .hasCapturingGroup (node .data .repeat .child ),
550+ .lookahead = > self .hasCapturingGroup (node .data .lookahead .child ),
551+ .lookbehind = > self .hasCapturingGroup (node .data .lookbehind .child ),
552+ else = > false ,
553+ };
554+ }
555+
556+ fn isEndAnchorOnly (self : * BacktrackEngine , node : * ast.Node ) bool {
557+ return switch (node .node_type ) {
558+ .anchor = > node .data .anchor == .end_line or node .data .anchor == .end_text ,
559+ .group = > self .isEndAnchorOnly (node .data .group .child ),
560+ .concat = > self .canMatchEmpty (node .data .concat .left ) and self .isEndAnchorOnly (node .data .concat .right ),
561+ else = > false ,
562+ };
563+ }
564+
481565 /// Whether a subtree contains a group (whose captures a constrained re-match
482566 /// would need to set). Conservative: any group counts. When false, the
483567 /// capture-fixup re-match in matchConcat/collectAllMatches is pure waste and
@@ -584,6 +668,11 @@ pub const BacktrackEngine = struct {
584668 const right_has_choices = self .hasQuantifiers (c .right ) or self .hasAlternation (c .right );
585669
586670 if (left_has_choices or right_has_choices ) {
671+ const clean_captures = self .allocator .alloc (CaptureGroup , self .captures .len ) catch return ;
672+ defer self .allocator .free (clean_captures );
673+ @memcpy (clean_captures , self .captures );
674+ const before_len = positions .items .len ;
675+
587676 // Collect all possible left-side endings
588677 var left_positions = std .ArrayList (usize ).initCapacity (self .allocator , 0 ) catch return ;
589678 defer left_positions .deinit (self .allocator );
@@ -624,6 +713,24 @@ pub const BacktrackEngine = struct {
624713
625714 @memcpy (self .captures , saved );
626715 }
716+ if (positions .items .len == before_len and ! right_has_choices and self .needsAnchoredRepeatedAlternationRecovery (c .left , c .right )) {
717+ var target = self .input .len ;
718+ while (target >= pos ) {
719+ @memcpy (self .captures , clean_captures );
720+ const screened = self .matchNode (c .right , target ) orelse {
721+ if (target == pos ) break ;
722+ target -= 1 ;
723+ continue ;
724+ };
725+ @memcpy (self .captures , clean_captures );
726+ if (self .matchNodeConstrained (c .left , pos , target )) {
727+ try positions .append (self .allocator , screened );
728+ }
729+ if (target == pos ) break ;
730+ target -= 1 ;
731+ }
732+ }
733+ @memcpy (self .captures , clean_captures );
627734 } else {
628735 // No quantifiers in either side, single match
629736 if (self .matchNode (node , pos )) | end | {
@@ -658,8 +765,8 @@ pub const BacktrackEngine = struct {
658765 }
659766 }
660767
661- fn collectGreedyStarMatches (self : * BacktrackEngine , child : * ast.Node , pos : usize , positions : * std .ArrayList (usize )) ! void {
662- // Collect all matches from longest to shortest
768+ fn collectGreedyStarMatches (self : * BacktrackEngine , child : * ast.Node , pos : usize , positions : * std .ArrayList (usize )) anyerror ! void {
769+ // Collect all matches from longest to shortest.
663770 var all_positions = std .ArrayList (usize ).initCapacity (self .allocator , 0 ) catch return ;
664771 defer all_positions .deinit (self .allocator );
665772
@@ -673,15 +780,15 @@ pub const BacktrackEngine = struct {
673780 try all_positions .append (self .allocator , current_pos );
674781 }
675782
676- // Return in reverse order (greedy: longest first)
783+ // Return in reverse order (greedy: longest first).
677784 var i : usize = all_positions .items .len ;
678785 while (i > 0 ) {
679786 i -= 1 ;
680787 try positions .append (self .allocator , all_positions .items [i ]);
681788 }
682789 }
683790
684- fn collectLazyStarMatches (self : * BacktrackEngine , child : * ast.Node , pos : usize , positions : * std .ArrayList (usize )) ! void {
791+ fn collectLazyStarMatches (self : * BacktrackEngine , child : * ast.Node , pos : usize , positions : * std .ArrayList (usize )) anyerror ! void {
685792 // Collect all matches from shortest to longest
686793 try positions .append (self .allocator , pos ); // zero matches first
687794
0 commit comments